Title: [264813] trunk/Tools
Revision
264813
Author
commit-qu...@webkit.org
Date
2020-07-24 00:30:32 -0700 (Fri, 24 Jul 2020)

Log Message

Add a GNU parallel runner
https://bugs.webkit.org/show_bug.cgi?id=214356

Patch by Angelos Oikonomopoulos <ange...@igalia.com> on 2020-07-24
Reviewed by Keith Miller.

At the moment, run-jsc-stress-tests uses make as a (local) job scheduling
engine (i.e. doesn't make use of the dependency tracking at all). However,
this causes problems for the distributed job scheduling we want to do when
using --remote.

If a remote is down, the tests will fail.
If a remote goes down during testing, the tests will fail.

There is no reason not to ignore remotes that are unavailable. What's more,
we should be able to reschedule jobs that were in the middle of execution when
a remote host goes down.

This patch tries to leverage GNU parallel as the execution engine to
hopefully transparently handle some of those failure scenarios.

* Scripts/run-_javascript_core-tests:
(runJSCStressTests):
* Scripts/run-jsc-stress-tests:

Modified Paths

Diff

Modified: trunk/Tools/ChangeLog (264812 => 264813)


--- trunk/Tools/ChangeLog	2020-07-24 05:31:56 UTC (rev 264812)
+++ trunk/Tools/ChangeLog	2020-07-24 07:30:32 UTC (rev 264813)
@@ -1,3 +1,29 @@
+2020-07-24  Angelos Oikonomopoulos  <ange...@igalia.com>
+
+        Add a GNU parallel runner
+        https://bugs.webkit.org/show_bug.cgi?id=214356
+
+        Reviewed by Keith Miller.
+
+        At the moment, run-jsc-stress-tests uses make as a (local) job scheduling
+        engine (i.e. doesn't make use of the dependency tracking at all). However,
+        this causes problems for the distributed job scheduling we want to do when
+        using --remote.
+
+        If a remote is down, the tests will fail.
+        If a remote goes down during testing, the tests will fail.
+
+        There is no reason not to ignore remotes that are unavailable. What's more,
+        we should be able to reschedule jobs that were in the middle of execution when
+        a remote host goes down.
+
+        This patch tries to leverage GNU parallel as the execution engine to
+        hopefully transparently handle some of those failure scenarios.
+
+        * Scripts/run-_javascript_core-tests:
+        (runJSCStressTests):
+        * Scripts/run-jsc-stress-tests:
+
 2020-07-23  Wenson Hsieh  <wenson_hs...@apple.com>
 
         Tapping QuickType suggestions for a misspelled word does nothing in Mail compose

Modified: trunk/Tools/Scripts/run-_javascript_core-tests (264812 => 264813)


--- trunk/Tools/Scripts/run-_javascript_core-tests	2020-07-24 05:31:56 UTC (rev 264812)
+++ trunk/Tools/Scripts/run-_javascript_core-tests	2020-07-24 07:30:32 UTC (rev 264813)
@@ -60,6 +60,7 @@
 my $shellRunner;
 my $makeRunner;
 my $rubyRunner;
+my $gnuParallelRunner;
 my $testWriter;
 my $memoryLimited;
 my $reportExecutionTime;
@@ -338,6 +339,7 @@
     'shell-runner' => \$shellRunner,
     'make-runner' => \$makeRunner,
     'ruby-runner' => \$rubyRunner,
+    'gnu-parallel-runner' => \$gnuParallelRunner,
     'test-writer=s' => \$testWriter,
     'memory-limited' => \$memoryLimited,
     'report-execution-time' => \$reportExecutionTime,
@@ -827,6 +829,10 @@
         push(@jscStressDriverCmd, "--ruby-runner");
     }
 
+    if ($gnuParallelRunner) {
+        push(@jscStressDriverCmd, "--gnu-parallel-runner");
+    }
+
     if ($testWriter) {
         push(@jscStressDriverCmd, "--test-writer");
         push(@jscStressDriverCmd, $testWriter);

Modified: trunk/Tools/Scripts/run-jsc-stress-tests (264812 => 264813)


--- trunk/Tools/Scripts/run-jsc-stress-tests	2020-07-24 05:31:56 UTC (rev 264812)
+++ trunk/Tools/Scripts/run-jsc-stress-tests	2020-07-24 07:30:32 UTC (rev 264813)
@@ -27,6 +27,7 @@
 require 'getoptlong'
 require 'pathname'
 require 'rbconfig'
+require 'tempfile'
 require 'uri'
 require 'yaml'
 
@@ -86,9 +87,15 @@
     $stderr.puts ">> #{commandArray}"
 end
 
+$ignoreNextCommandExecutionExitCode = false
+
 def mysys(*cmd)
-    printCommandArray(*cmd) if $verbosity >= 1
-    raise "Command failed: #{$?.inspect}" unless system(*cmd)
+    begin
+        printCommandArray(*cmd) if $verbosity >= 1
+        raise "Command failed: #{$?.inspect}" unless (system(*cmd) or $ignoreNextCommandExecutionExitCode)
+    ensure
+        $ignoreNextCommandExecutionExitCode = false
+    end
 end
 
 def escapeAll(array)
@@ -187,6 +194,7 @@
                ['--shell-runner', GetoptLong::NO_ARGUMENT],
                ['--make-runner', GetoptLong::NO_ARGUMENT],
                ['--ruby-runner', GetoptLong::NO_ARGUMENT],
+               ['--gnu-parallel-runner', GetoptLong::NO_ARGUMENT],
                ['--test-writer', GetoptLong::REQUIRED_ARGUMENT],
                ['--remote', GetoptLong::REQUIRED_ARGUMENT],
                ['--remote-config-file', GetoptLong::REQUIRED_ARGUMENT],
@@ -232,6 +240,8 @@
         $testRunnerType = :make
     when '--ruby-runner'
         $testRunnerType = :ruby
+    when '--gnu-parallel-runner'
+        $testRunnerType = :gnuparallel
     when '--test-writer'
         $testWriter = arg
     when '--remote'
@@ -499,8 +509,8 @@
     end
 end
 
-if $remoteHosts.length > 1 and $testRunnerType != :make
-    raise "Multiple remote hosts only supported with make runner"
+if $remoteHosts.length > 1 and ($testRunnerType != :make) and ($testRunnerType != :gnuparallel)
+    raise "Multiple remote hosts only supported with the make or gnu-parallel runners"
 end
 
 if $hostOS == "playstation" && $testWriter == "default"
@@ -1955,6 +1965,8 @@
         prepareShellTestRunner
     when :ruby
         prepareRubyTestRunner
+    when :gnuparallel
+        prepareGnuParallelTestRunner
     else
         raise "Unknown test runner type: #{$testRunnerType.to_s}"
     end
@@ -2089,25 +2101,42 @@
     end
 end
 
+def getRemoteDirectoryIfNeeded(remoteIndex)
+    remoteHost = $remoteHosts[remoteIndex]
+    if !remoteHost.remoteDirectory
+        remoteHost.remoteDirectory = JSON::parse(sshRead("cat ~/.bencher", remoteIndex))["tempPath"]
+    end
+end
+
+def copyBundleToRemote(remoteHost)
+    mysys("ssh", "-o", "NoHostAuthenticationForLocalhost=yes", "-p", remoteHost.port.to_s, "#{remoteHost.user}@#{remoteHost.host}", "mkdir -p #{remoteHost.remoteDirectory}")
+    mysys("scp", "-o", "NoHostAuthenticationForLocalhost=yes", "-P", remoteHost.port.to_s, ($outputDir.dirname + $tarFileName).to_s, "#{remoteHost.user}@#{remoteHost.host}:#{remoteHost.remoteDirectory}")
+end
+
+def exportBaseEnvironmentVariables
+    dyldFrameworkPath = "$(cd #{$testingFrameworkPath.dirname}; pwd)"
+    ldLibraryPath = "$(pwd)/#{$outputDir.basename}/#{$jscPath.dirname}"
+    [
+        "export DYLD_FRAMEWORK_PATH=#{Shellwords.shellescape(dyldFrameworkPath)} && ",
+        "export LD_LIBRARY_PATH=#{Shellwords.shellescape(ldLibraryPath)} &&",
+        "export JSCTEST_timeout=#{Shellwords.shellescape(ENV['JSCTEST_timeout'])} && ",
+        "export JSCTEST_hardTimeout=#{Shellwords.shellescape(ENV['JSCTEST_hardTimeout'])} && ",
+        "export JSCTEST_memoryLimit=#{Shellwords.shellescape(ENV['JSCTEST_memoryLimit'])} && ",
+        "export TZ=#{Shellwords.shellescape(ENV['TZ'])} && ",
+    ].join("")
+end
+
 def runTestRunner(remoteIndex=0)
     if $remote
         remoteHost = $remoteHosts[remoteIndex]
-        if !remoteHost.remoteDirectory
-            remoteHost.remoteDirectory = JSON::parse(sshRead("cat ~/.bencher", remoteIndex))["tempPath"]
-        end
-        mysys("ssh", "-o", "NoHostAuthenticationForLocalhost=yes", "-p", remoteHost.port.to_s, "#{remoteHost.user}@#{remoteHost.host}", "mkdir -p #{remoteHost.remoteDirectory}")
-        mysys("scp", "-o", "NoHostAuthenticationForLocalhost=yes", "-P", remoteHost.port.to_s, ($outputDir.dirname + $tarFileName).to_s, "#{remoteHost.user}@#{remoteHost.host}:#{remoteHost.remoteDirectory}")
+        getRemoteDirectoryIfNeeded(remoteIndex)
+        copyBundleToRemote(remoteHost)
         remoteScript = "\""
         remoteScript += "cd #{remoteHost.remoteDirectory} && "
         remoteScript += "rm -rf #{$outputDir.basename} && "
         remoteScript += "tar xzf #{$tarFileName} && "
         remoteScript += "cd #{$outputDir.basename}/.runner && "
-        remoteScript += "export DYLD_FRAMEWORK_PATH=\\\"\\$(cd #{$testingFrameworkPath.dirname}; pwd)\\\" && "
-        remoteScript += "export LD_LIBRARY_PATH=#{remoteHost.remoteDirectory}/#{$outputDir.basename}/#{$jscPath.dirname} && "
-        remoteScript += "export JSCTEST_timeout=#{Shellwords.shellescape(ENV['JSCTEST_timeout'])} && "
-        remoteScript += "export JSCTEST_hardTimeout=#{Shellwords.shellescape(ENV['JSCTEST_hardTimeout'])} && "
-        remoteScript += "export JSCTEST_memoryLimit=#{Shellwords.shellescape(ENV['JSCTEST_memoryLimit'])} && "
-        remoteScript += "export TZ=#{Shellwords.shellescape(ENV['TZ'])} && "
+        remoteScript += exportBaseEnvironmentVariables
         $envVars.each { |var| remoteScript += "export " << var << "\n" }
         remoteScript += "#{testRunnerCommand(remoteIndex)}\""
         runAndMonitorTestRunnerCommand("ssh", "-o", "NoHostAuthenticationForLocalhost=yes", "-p", remoteHost.port.to_s, "#{remoteHost.user}@#{remoteHost.host}", remoteScript)
@@ -2277,6 +2306,18 @@
     compressBundle
 end
 
+def forEachRemote(&blk)
+    threads = []
+    $remoteHosts.each_index {
+        | index |
+        remoteHost = $remoteHosts[index]
+        threads << Thread.new {
+            blk.call(index, remoteHost)
+        }
+    }
+    threads.each { |thr| thr.join }
+end
+
 def runRemote
     raise unless $remote
 
@@ -2286,22 +2327,158 @@
         prepareTestRunner(index)
     }
     compressBundle
-    threads = []
-    $remoteHosts.each_index {
+    forEachRemote {
         | index |
-        threads << Thread.new {
-            runTestRunner(index)
+        runTestRunner(index)
+    }
+    detectFailures
+end
+
+def prepareGnuParallelTestRunner
+    path = $runnerDir + "parallel-tests"
+    FileUtils.mkdir_p($runnerDir)
+
+    File.open(path, "w") {
+        | outp |
+        $runlist.each {
+            | plan |
+            outp.puts("./test_script_#{plan.index}")
         }
     }
-    threads.each { |thr| thr.join }
+end
+
+def withGnuParallelSshWrapper(&blk)
+    Tempfile.open('ssh-wrapper', $runnerDir) {
+        | wrapper |
+        head =
+<<'EOF'
+#!/bin/sh
+
+remotedir="$1"
+shift
+
+remoteport="$1"
+shift
+
+remoteuser="$1"
+shift
+
+remotehost="$1"
+shift
+
+if test "x$1" != "x--"; then
+   echo "Expected '--' at this position, instead got $1" 1>&2
+   exit 3
+fi
+shift
+EOF
+        wrapper.puts(head +
+                     "echo \"$@\" | ssh -o ControlPath=./%C -o ControlMaster=auto -o ControlPersist=10m -o NoHostAuthenticationForLocalhost=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p \"$remoteport\" -l \"$remoteuser\" -o RemoteCommand=\"cd '$remotedir' && sh -s\" \"$remotehost\""
+        )
+        FileUtils.chmod("ugo=rx", wrapper.path)
+        wrapper.close # Avoid ETXTBUSY
+        blk.call(wrapper.path)
+    }
+end
+
+def withGnuParallelSshLoginFile(&blk)
+    withGnuParallelSshWrapper {
+        | wrapper |
+        Tempfile.open('slf', $runnerDir) {
+            | tf |
+            $remoteHosts.each {
+                | remoteHost |
+                tf.puts("#{wrapper} #{remoteHost.remoteDirectory} #{remoteHost.port} #{remoteHost.user} #{remoteHost.host}")
+            }
+            tf.flush
+            blk.call(tf.path)
+        }
+    }
+end
+
+
+def runSameCommandOnRemotes(cmd)
+    Tempfile.open('gnu-parallel-commands', $runnerDir) {
+        | commands |
+        commands.puts(cmd)
+        commands.flush
+        withGnuParallelSshLoginFile {
+            | slf |
+            cmd = [
+                "parallel",
+                "--nonall",
+                "-j0",
+                "--slf", slf
+            ] + ["-a", commands.path]
+            return mysys(*cmd)
+        }
+    }
+end
+
+def unpackBundleGnuParallel
+    runSameCommandOnRemotes("rm -rf #{$outputDir.basename} && " +
+                            "tar xzf #{$tarFileName}")
+end
+
+def runGnuParallelRunner
+    inputs = $runnerDir + "parallel-tests"
+    timeout = 300
+    if ENV["JSCTEST_timeout"]
+        timeout = ENV["JSCTEST_timeout"].to_f.ceil.to_i
+    end
+    withGnuParallelSshLoginFile {
+        | slf |
+        cmd = [
+            "parallel",
+            # We add 1 to make sure we always have waiting jobs and
+            # don't run into stalls due to ssh latency. However, we
+            # want to respect numChildProcesses, so we don't just use
+            # the -j +1 GNU parallel idiom.
+            "-j", "#{$numChildProcesses + 1}",
+            "--retries 5",
+            "--line-buffer", # we know our output is line-oriented
+            "--slf", slf,
+            "--timeout", timeout.to_s,
+            "-a", inputs,
+            "'cd #{$outputDir.basename}/.runner && " +
+            exportBaseEnvironmentVariables +
+            $envVars.collect { |var | "export #{var} &&"}.join("") +
+            "sh '"
+        ]
+        $ignoreNextCommandExecutionExitCode = true
+        runAndMonitorTestRunnerCommand(*cmd)
+    }
+end
+
+def runGnuParallel
+    raise unless $remote
+    prepareBundle
+    prepareTestRunner
+    compressBundle
+    forEachRemote {
+        |remoteIndex, remoteHost|
+        getRemoteDirectoryIfNeeded(remoteIndex)
+        copyBundleToRemote(remoteHost)
+    }
+    unpackBundleGnuParallel
+    runGnuParallelRunner
     detectFailures
 end
 
 puts
+
+if $testRunnerType == :gnuparallel
+    raise unless $remote
+end
+
 if $bundle
     runBundle
 elsif $remote
-    runRemote
+    if $testRunnerType == :gnuparallel
+        runGnuParallel
+    else
+        runRemote
+    end
 elsif $tarball
     runTarball
 else
_______________________________________________
webkit-changes mailing list
webkit-changes@lists.webkit.org
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to