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);