diff --git a/.github/workflows/perf.yml b/.github/workflows/perf.yml index 42d7d4dc6..a31d7f191 100644 --- a/.github/workflows/perf.yml +++ b/.github/workflows/perf.yml @@ -6,19 +6,19 @@ jobs: perf: runs-on: Ubuntu-18.04 steps: - - name: Checkout - uses: actions/checkout@master - with: - fetch-depth: 1 - - name: Run Benchmark - run: | - git clone https://github.com/kylef/swiftenv.git ~/.swiftenv - export SWIFTENV_ROOT="$HOME/.swiftenv" - export PATH="$SWIFTENV_ROOT/bin:$PATH" - eval "$(swiftenv init -)" - swiftenv install $TOOLCHAIN_DOWNLOAD - make perf-tester - node ci/perf-tester - env: - TOOLCHAIN_DOWNLOAD: https://github.com/swiftwasm/swift/releases/download/swift-wasm-5.3-SNAPSHOT-2020-08-10-a/swift-wasm-5.3-SNAPSHOT-2020-08-10-a-linux.tar.gz - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Checkout + uses: actions/checkout@master + with: + fetch-depth: 1 + - name: Run Benchmark + run: | + git clone https://github.com/kylef/swiftenv.git ~/.swiftenv + export SWIFTENV_ROOT="$HOME/.swiftenv" + export PATH="$SWIFTENV_ROOT/bin:$PATH" + eval "$(swiftenv init -)" + swiftenv install $TOOLCHAIN_DOWNLOAD + make perf-tester + node ci/perf-tester + env: + TOOLCHAIN_DOWNLOAD: https://github.com/swiftwasm/swift/releases/download/swift-wasm-5.3-SNAPSHOT-2020-08-10-a/swift-wasm-5.3-SNAPSHOT-2020-08-10-a-linux.tar.gz + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 53fa87dd3..a82920b40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,5 @@ # Unreleased - # 0.7.0 (25 September 2020) This release adds multiple new types bridged from JavaScript, namely `JSError`, `JSDate`, `JSTimer` (which corresponds to `setTimeout`/`setInterval` calls and manages closure lifetime for you), `JSString` and `JSPromise`. We now also have [documentation published automatically](https://swiftwasm.github.io/JavaScriptKit/) for the main branch. diff --git a/ci/perf-tester/src/index.js b/ci/perf-tester/src/index.js index e2427305a..f9495303d 100644 --- a/ci/perf-tester/src/index.js +++ b/ci/perf-tester/src/index.js @@ -20,206 +20,237 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -const { setFailed, startGroup, endGroup, debug } = require('@actions/core'); -const { GitHub, context } = require('@actions/github'); -const { exec } = require('@actions/exec'); -const { getInput, runBenchmark, averageBenchmarks, toDiff, diffTable, toBool } = require('./utils.js'); +const { setFailed, startGroup, endGroup, debug } = require("@actions/core"); +const { GitHub, context } = require("@actions/github"); +const { exec } = require("@actions/exec"); +const { + getInput, + runBenchmark, + averageBenchmarks, + toDiff, + diffTable, + toBool, +} = require("./utils.js"); + +const benchmarkParallel = 2; +const benchmarkSerial = 2; +const runBenchmarks = async () => { + let results = []; + for (let i = 0; i < benchmarkSerial; i++) { + results = results.concat( + await Promise.all(Array(benchmarkParallel).fill().map(runBenchmark)) + ); + } + return averageBenchmarks(results); +}; async function run(octokit, context, token) { - const { number: pull_number } = context.issue; - - const pr = context.payload.pull_request; - try { - debug('pr' + JSON.stringify(pr, null, 2)); - } catch (e) { } - if (!pr) { - throw Error('Could not retrieve PR information. Only "pull_request" triggered workflows are currently supported.'); - } - - console.log(`PR #${pull_number} is targetted at ${pr.base.ref} (${pr.base.sha})`); - - const buildScript = getInput('build-script'); - startGroup(`[current] Build using '${buildScript}'`); - await exec(buildScript); - endGroup(); - - startGroup(`[current] Running benchmark`); - const newBenchmarks = await Promise.all([runBenchmark(), runBenchmark()]).then(averageBenchmarks); - endGroup(); - - startGroup(`[base] Checkout target branch`); - let baseRef; - try { - baseRef = context.payload.base.ref; - if (!baseRef) throw Error('missing context.payload.pull_request.base.ref'); - await exec(`git fetch -n origin ${context.payload.pull_request.base.ref}`); - console.log('successfully fetched base.ref'); - } catch (e) { - console.log('fetching base.ref failed', e.message); - try { - await exec(`git fetch -n origin ${pr.base.sha}`); - console.log('successfully fetched base.sha'); - } catch (e) { - console.log('fetching base.sha failed', e.message); - try { - await exec(`git fetch -n`); - } catch (e) { - console.log('fetch failed', e.message); - } - } - } - - console.log('checking out and building base commit'); - try { - if (!baseRef) throw Error('missing context.payload.base.ref'); - await exec(`git reset --hard ${baseRef}`); - } - catch (e) { - await exec(`git reset --hard ${pr.base.sha}`); - } - endGroup(); - - startGroup(`[base] Build using '${buildScript}'`); - await exec(buildScript); - endGroup(); - - startGroup(`[base] Running benchmark`); - const oldBenchmarks = await Promise.all([runBenchmark(), runBenchmark()]).then(averageBenchmarks); - endGroup(); - - const diff = toDiff(oldBenchmarks, newBenchmarks); - - const markdownDiff = diffTable(diff, { - collapseUnchanged: true, - omitUnchanged: false, - showTotal: true, - minimumChangeThreshold: parseInt(getInput('minimum-change-threshold'), 10) - }); - - let outputRawMarkdown = false; - - const commentInfo = { - ...context.repo, - issue_number: pull_number - }; - - const comment = { - ...commentInfo, - body: markdownDiff + '\n\nperformance-action' - }; - - if (toBool(getInput('use-check'))) { - if (token) { - const finish = await createCheck(octokit, context); - await finish({ - conclusion: 'success', - output: { - title: `Compressed Size Action`, - summary: markdownDiff - } - }); - } - else { - outputRawMarkdown = true; - } - } - else { - startGroup(`Updating stats PR comment`); - let commentId; - try { - const comments = (await octokit.issues.listComments(commentInfo)).data; - for (let i = comments.length; i--;) { - const c = comments[i]; - if (c.user.type === 'Bot' && /[\s\n]*performance-action/.test(c.body)) { - commentId = c.id; - break; - } - } - } - catch (e) { - console.log('Error checking for previous comments: ' + e.message); - } - - if (commentId) { - console.log(`Updating previous comment #${commentId}`) - try { - await octokit.issues.updateComment({ - ...context.repo, - comment_id: commentId, - body: comment.body - }); - } - catch (e) { - console.log('Error editing previous comment: ' + e.message); - commentId = null; - } - } - - // no previous or edit failed - if (!commentId) { - console.log('Creating new comment'); - try { - await octokit.issues.createComment(comment); - } catch (e) { - console.log(`Error creating comment: ${e.message}`); - console.log(`Submitting a PR review comment instead...`); - try { - const issue = context.issue || pr; - await octokit.pulls.createReview({ - owner: issue.owner, - repo: issue.repo, - pull_number: issue.number, - event: 'COMMENT', - body: comment.body - }); - } catch (e) { - console.log('Error creating PR review.'); - outputRawMarkdown = true; - } - } - } - endGroup(); - } - - if (outputRawMarkdown) { - console.log(` + const { number: pull_number } = context.issue; + + const pr = context.payload.pull_request; + try { + debug("pr" + JSON.stringify(pr, null, 2)); + } catch (e) {} + if (!pr) { + throw Error( + 'Could not retrieve PR information. Only "pull_request" triggered workflows are currently supported.' + ); + } + + console.log( + `PR #${pull_number} is targetted at ${pr.base.ref} (${pr.base.sha})` + ); + + const buildScript = getInput("build-script"); + startGroup(`[current] Build using '${buildScript}'`); + await exec(buildScript); + endGroup(); + + startGroup(`[current] Running benchmark`); + const newBenchmarks = await runBenchmarks(); + endGroup(); + + startGroup(`[base] Checkout target branch`); + let baseRef; + try { + baseRef = context.payload.base.ref; + if (!baseRef) + throw Error("missing context.payload.pull_request.base.ref"); + await exec( + `git fetch -n origin ${context.payload.pull_request.base.ref}` + ); + console.log("successfully fetched base.ref"); + } catch (e) { + console.log("fetching base.ref failed", e.message); + try { + await exec(`git fetch -n origin ${pr.base.sha}`); + console.log("successfully fetched base.sha"); + } catch (e) { + console.log("fetching base.sha failed", e.message); + try { + await exec(`git fetch -n`); + } catch (e) { + console.log("fetch failed", e.message); + } + } + } + + console.log("checking out and building base commit"); + try { + if (!baseRef) throw Error("missing context.payload.base.ref"); + await exec(`git reset --hard ${baseRef}`); + } catch (e) { + await exec(`git reset --hard ${pr.base.sha}`); + } + endGroup(); + + startGroup(`[base] Build using '${buildScript}'`); + await exec(buildScript); + endGroup(); + + startGroup(`[base] Running benchmark`); + const oldBenchmarks = await runBenchmarks(); + endGroup(); + + const diff = toDiff(oldBenchmarks, newBenchmarks); + + const markdownDiff = diffTable(diff, { + collapseUnchanged: true, + omitUnchanged: false, + showTotal: true, + minimumChangeThreshold: parseInt( + getInput("minimum-change-threshold"), + 10 + ), + }); + + let outputRawMarkdown = false; + + const commentInfo = { + ...context.repo, + issue_number: pull_number, + }; + + const comment = { + ...commentInfo, + body: + markdownDiff + + '\n\nperformance-action', + }; + + if (toBool(getInput("use-check"))) { + if (token) { + const finish = await createCheck(octokit, context); + await finish({ + conclusion: "success", + output: { + title: `Compressed Size Action`, + summary: markdownDiff, + }, + }); + } else { + outputRawMarkdown = true; + } + } else { + startGroup(`Updating stats PR comment`); + let commentId; + try { + const comments = (await octokit.issues.listComments(commentInfo)) + .data; + for (let i = comments.length; i--; ) { + const c = comments[i]; + if ( + c.user.type === "Bot" && + /[\s\n]*performance-action/.test(c.body) + ) { + commentId = c.id; + break; + } + } + } catch (e) { + console.log("Error checking for previous comments: " + e.message); + } + + if (commentId) { + console.log(`Updating previous comment #${commentId}`); + try { + await octokit.issues.updateComment({ + ...context.repo, + comment_id: commentId, + body: comment.body, + }); + } catch (e) { + console.log("Error editing previous comment: " + e.message); + commentId = null; + } + } + + // no previous or edit failed + if (!commentId) { + console.log("Creating new comment"); + try { + await octokit.issues.createComment(comment); + } catch (e) { + console.log(`Error creating comment: ${e.message}`); + console.log(`Submitting a PR review comment instead...`); + try { + const issue = context.issue || pr; + await octokit.pulls.createReview({ + owner: issue.owner, + repo: issue.repo, + pull_number: issue.number, + event: "COMMENT", + body: comment.body, + }); + } catch (e) { + console.log("Error creating PR review."); + outputRawMarkdown = true; + } + } + } + endGroup(); + } + + if (outputRawMarkdown) { + console.log( + ` Error: performance-action was unable to comment on your PR. This can happen for PR's originating from a fork without write permissions. You can copy the size table directly into a comment using the markdown below: \n\n${comment.body}\n\n - `.replace(/^(\t| )+/gm, '')); - } + `.replace(/^(\t| )+/gm, "") + ); + } - console.log('All done!'); + console.log("All done!"); } - // create a check and return a function that updates (completes) it async function createCheck(octokit, context) { - const check = await octokit.checks.create({ - ...context.repo, - name: 'Compressed Size', - head_sha: context.payload.pull_request.head.sha, - status: 'in_progress', - }); - - return async details => { - await octokit.checks.update({ - ...context.repo, - check_run_id: check.data.id, - completed_at: new Date().toISOString(), - status: 'completed', - ...details - }); - }; + const check = await octokit.checks.create({ + ...context.repo, + name: "Compressed Size", + head_sha: context.payload.pull_request.head.sha, + status: "in_progress", + }); + + return async (details) => { + await octokit.checks.update({ + ...context.repo, + check_run_id: check.data.id, + completed_at: new Date().toISOString(), + status: "completed", + ...details, + }); + }; } (async () => { - try { - const token = getInput('repo-token', { required: true }); - const octokit = new GitHub(token); - await run(octokit, context, token); - } catch (e) { - setFailed(e.message); - } + try { + const token = getInput("repo-token", { required: true }); + const octokit = new GitHub(token); + await run(octokit, context, token); + } catch (e) { + setFailed(e.message); + } })(); diff --git a/ci/perf-tester/src/utils.js b/ci/perf-tester/src/utils.js index b92bdcd44..49fce603e 100644 --- a/ci/perf-tester/src/utils.js +++ b/ci/perf-tester/src/utils.js @@ -20,50 +20,53 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -const fs = require('fs'); -const { exec } = require('@actions/exec'); - -const getInput = key => ({ - 'build-script': 'make bootstrap benchmark_setup', - benchmark: 'make -s run_benchmark', - 'minimum-change-threshold': 5, - 'use-check': 'no', - 'repo-token': process.env.GITHUB_TOKEN -})[key] -exports.getInput = getInput +const fs = require("fs"); +const { exec } = require("@actions/exec"); + +const getInput = (key) => + ({ + "build-script": "make bootstrap benchmark_setup", + benchmark: "make -s run_benchmark", + "minimum-change-threshold": 5, + "use-check": "no", + "repo-token": process.env.GITHUB_TOKEN, + }[key]); +exports.getInput = getInput; exports.runBenchmark = async () => { - let benchmarkBuffers = [] - await exec(getInput('benchmark'), [], { - listeners: { - stdout: data => benchmarkBuffers.push(data) - } - }); - const output = Buffer.concat(benchmarkBuffers).toString('utf8') - return parse(output) -} - -const firstLineRe = /^Running '(.+)' \.\.\.$/ -const secondLineRe = /^done ([\d.]+) ms$/ + let benchmarkBuffers = []; + await exec(getInput("benchmark"), [], { + listeners: { + stdout: (data) => benchmarkBuffers.push(data), + }, + }); + const output = Buffer.concat(benchmarkBuffers).toString("utf8"); + return parse(output); +}; + +const firstLineRe = /^Running '(.+)' \.\.\.$/; +const secondLineRe = /^done ([\d.]+) ms$/; function parse(benchmarkData) { - const lines = benchmarkData.trim().split('\n') - const benchmarks = Object.create(null) - for (let i = 0; i < lines.length - 1; i += 2) { - const [, name] = firstLineRe.exec(lines[i]) - const [, time] = secondLineRe.exec(lines[i + 1]) - benchmarks[name] = Math.round(parseFloat(time)) - } - return benchmarks + const lines = benchmarkData.trim().split("\n"); + const benchmarks = Object.create(null); + for (let i = 0; i < lines.length - 1; i += 2) { + const [, name] = firstLineRe.exec(lines[i]); + const [, time] = secondLineRe.exec(lines[i + 1]); + benchmarks[name] = Math.round(parseFloat(time)); + } + return benchmarks; } exports.averageBenchmarks = (benchmarks) => { - const result = Object.create(null) - for (const key of Object.keys(benchmarks[0])) { - result[key] = benchmarks.reduce((acc, bench) => acc + bench[key], 0) / benchmarks.length - } - return result -} + const result = Object.create(null); + for (const key of Object.keys(benchmarks[0])) { + result[key] = + benchmarks.reduce((acc, bench) => acc + bench[key], 0) / + benchmarks.length; + } + return result; +}; /** * @param {{[key: string]: number}} before @@ -71,42 +74,42 @@ exports.averageBenchmarks = (benchmarks) => { * @return {Diff[]} */ exports.toDiff = (before, after) => { - const names = [...new Set([...Object.keys(before), ...Object.keys(after)])] - return names.map(name => { - const timeBefore = before[name] || 0 - const timeAfter = after[name] || 0 - const delta = timeAfter - timeBefore - return { name, time: timeAfter, delta } - }) -} - + const names = [...new Set([...Object.keys(before), ...Object.keys(after)])]; + return names.map((name) => { + const timeBefore = before[name] || 0; + const timeAfter = after[name] || 0; + const delta = timeAfter - timeBefore; + return { name, time: timeAfter, delta }; + }); +}; /** * @param {number} delta * @param {number} difference */ function getDeltaText(delta, difference) { - let deltaText = (delta > 0 ? '+' : '') + delta.toLocaleString('en-US') + 'ms'; - if (delta && Math.abs(delta) > 1) { - deltaText += ` (${Math.abs(difference)}%)`; - } - return deltaText; + let deltaText = + (delta > 0 ? "+" : "") + delta.toLocaleString("en-US") + "ms"; + if (delta && Math.abs(delta) > 1) { + deltaText += ` (${Math.abs(difference)}%)`; + } + return deltaText; } /** * @param {number} difference */ function iconForDifference(difference) { - let icon = ''; - if (difference >= 50) icon = '🆘'; - else if (difference >= 20) icon = '🚨'; - else if (difference >= 10) icon = 'âš ī¸'; - else if (difference >= 5) icon = '🔍'; - else if (difference <= -50) icon = '🏆'; - else if (difference <= -20) icon = '🎉'; - else if (difference <= -10) icon = '👏'; - else if (difference <= -5) icon = '✅'; - return icon; + let icon = ""; + if (difference >= 50) icon = "🆘"; + else if (difference >= 20) icon = "🚨"; + else if (difference >= 10) icon = "âš ī¸"; + else if (difference >= 5) icon = "🔍"; + else if (difference <= -50) icon = "🏆"; + else if (difference <= -20) icon = "🎉"; + else if (difference <= -10) icon = "👏"; + else if (difference <= -5) icon = "✅"; + return icon; } /** @@ -114,31 +117,33 @@ function iconForDifference(difference) { * @param {string[]} rows */ function markdownTable(rows) { - if (rows.length == 0) { - return ''; - } - - // Skip all empty columns - while (rows.every(columns => !columns[columns.length - 1])) { - for (const columns of rows) { - columns.pop(); - } - } - - const [firstRow] = rows; - const columnLength = firstRow.length; - if (columnLength === 0) { - return ''; - } - - return [ - // Header - ['Test name', 'Duration', 'Change', ''].slice(0, columnLength), - // Align - [':---', ':---:', ':---:', ':---:'].slice(0, columnLength), - // Body - ...rows - ].map(columns => `| ${columns.join(' | ')} |`).join('\n'); + if (rows.length == 0) { + return ""; + } + + // Skip all empty columns + while (rows.every((columns) => !columns[columns.length - 1])) { + for (const columns of rows) { + columns.pop(); + } + } + + const [firstRow] = rows; + const columnLength = firstRow.length; + if (columnLength === 0) { + return ""; + } + + return [ + // Header + ["Test name", "Duration", "Change", ""].slice(0, columnLength), + // Align + [":---", ":---:", ":---:", ":---:"].slice(0, columnLength), + // Body + ...rows, + ] + .map((columns) => `| ${columns.join(" | ")} |`) + .join("\n"); } /** @@ -157,55 +162,60 @@ function markdownTable(rows) { * @param {boolean} [options.omitUnchanged] * @param {number} [options.minimumChangeThreshold] */ -exports.diffTable = (tests, { showTotal, collapseUnchanged, omitUnchanged, minimumChangeThreshold }) => { - let changedRows = []; - let unChangedRows = []; - - let totalTime = 0; - let totalDelta = 0; - for (const file of tests) { - const { name, time, delta } = file; - totalTime += time; - totalDelta += delta; - - const difference = ((delta / time) * 100) | 0; - const isUnchanged = Math.abs(difference) < minimumChangeThreshold; - - if (isUnchanged && omitUnchanged) continue; - - const columns = [ - name, - time.toLocaleString('en-US') + 'ms', - getDeltaText(delta, difference), - iconForDifference(difference) - ]; - if (isUnchanged && collapseUnchanged) { - unChangedRows.push(columns); - } else { - changedRows.push(columns); - } - } - - let out = markdownTable(changedRows); - - if (unChangedRows.length !== 0) { - const outUnchanged = markdownTable(unChangedRows); - out += `\n\n
â„šī¸ View Unchanged\n\n${outUnchanged}\n\n
\n\n`; - } - - if (showTotal) { - const totalDifference = ((totalDelta / totalTime) * 100) | 0; - let totalDeltaText = getDeltaText(totalDelta, totalDifference); - let totalIcon = iconForDifference(totalDifference); - out = `**Total Time:** ${totalTime.toLocaleString('en-US')}ms\n\n${out}`; - out = `**Time Change:** ${totalDeltaText} ${totalIcon}\n\n${out}`; - } - - return out; -} +exports.diffTable = ( + tests, + { showTotal, collapseUnchanged, omitUnchanged, minimumChangeThreshold } +) => { + let changedRows = []; + let unChangedRows = []; + + let totalTime = 0; + let totalDelta = 0; + for (const file of tests) { + const { name, time, delta } = file; + totalTime += time; + totalDelta += delta; + + const difference = ((delta / time) * 100) | 0; + const isUnchanged = Math.abs(difference) < minimumChangeThreshold; + + if (isUnchanged && omitUnchanged) continue; + + const columns = [ + name, + time.toLocaleString("en-US") + "ms", + getDeltaText(delta, difference), + iconForDifference(difference), + ]; + if (isUnchanged && collapseUnchanged) { + unChangedRows.push(columns); + } else { + changedRows.push(columns); + } + } + + let out = markdownTable(changedRows); + + if (unChangedRows.length !== 0) { + const outUnchanged = markdownTable(unChangedRows); + out += `\n\n
â„šī¸ View Unchanged\n\n${outUnchanged}\n\n
\n\n`; + } + + if (showTotal) { + const totalDifference = ((totalDelta / totalTime) * 100) | 0; + let totalDeltaText = getDeltaText(totalDelta, totalDifference); + let totalIcon = iconForDifference(totalDifference); + out = `**Total Time:** ${totalTime.toLocaleString( + "en-US" + )}ms\n\n${out}`; + out = `**Time Change:** ${totalDeltaText} ${totalIcon}\n\n${out}`; + } + + return out; +}; /** * Convert a string "true"/"yes"/"1" argument value to a boolean * @param {string} v */ -exports.toBool = v => /^(1|true|yes)$/.test(v); +exports.toBool = (v) => /^(1|true|yes)$/.test(v);