reorganize things so its a little easier to follow.

This commit is contained in:
mdgriffith 2020-08-01 17:04:18 -04:00
parent aa550025d1
commit c685817eaa
6 changed files with 307 additions and 290 deletions

View File

@ -4,7 +4,7 @@ import * as path from 'path';
import { parseElm } from './parseElm';
import ts from 'typescript';
import { createCustomTypesTransformer } from './experiments/variantShapes';
import { Mode } from './types';
import { Mode, Transforms, ObjectUpdate } from './types';
import {
createFunctionInlineTransformer,
InlineContext,
@ -20,18 +20,13 @@ import { execSync } from 'child_process';
import {
createReplaceUtilsUpdateWithObjectSpread,
convertFunctionExpressionsToArrowFuncs,
NativeSpread,
} from './experiments/modernizeJS';
import { createRemoveUnusedLocalsTransform } from './experiments/removeUnusedLocals';
type TransformOptions = {
prepack: boolean;
};
export const compileAndTransform = async (
dir: string,
file: string,
options?: TransformOptions
options: Transforms
): Promise<{}> => {
// Compile examples in `testcases/*` folder as js
// Run whatever transformations we want on them, saving steps as `elm.{transformation}.js`
@ -71,25 +66,27 @@ export const compileAndTransform = async (
Mode.Prod
);
const transformations = removeDisabled([
[options.variantShapes, normalizeVariantShapes],
[
options.inlineFunctions,
createFunctionInlineTransformer(reportInlineTransformResult),
],
[
options.listLiterals,
createInlineListFromArrayTransformer(
InlineMode.UsingLiteralObjects(Mode.Prod)
),
],
includeObjectUpdate(options.objectUpdate),
[options.arrowFns, convertFunctionExpressionsToArrowFuncs],
[options.unusedValues, createRemoveUnusedLocalsTransform()],
]);
const {
transformed: [result],
diagnostics,
} = ts.transform(source, [
normalizeVariantShapes,
createFunctionInlineTransformer(reportInlineTransformResult),
createInlineListFromArrayTransformer(
InlineMode.UsingLiteralObjects(Mode.Prod)
// InlineMode.UsingLiteralObjects(Mode.Prod)
),
createReplaceUtilsUpdateWithObjectSpread(
NativeSpread.UseSpreadForUpdateAndOriginalRecord
),
convertFunctionExpressionsToArrowFuncs,
// This is awesome work, but disabling it for now to make things run faster
// createRemoveUnusedLocalsTransform(),
]);
} = ts.transform(source, transformations);
const printer = ts.createPrinter();
@ -106,16 +103,19 @@ export const compileAndTransform = async (
fs.writeFileSync(pathInOutput('elm.opt.js'), printer.printFile(initialJs));
if (options?.prepack) {
// console.log('here');
if (options.prepack) {
const { code } = prepackFileSync([pathInOutput('elm.opt.transformed.js')], {
debugNames: true,
inlineExpressions: true,
maxStackDepth: 1200, // that didn't help
});
// console.log('there', code.length);
fs.writeFileSync(pathInOutput('elm.opt.prepack.js'), code);
await minify(
pathInOutput('elm.opt.prepack.js'),
pathInOutput('elm.opt.prepack.min.js')
);
gzip(pathInOutput('elm.opt.prepack.min.js'));
}
await minify(pathInOutput('elm.opt.js'), pathInOutput('elm.opt.min.js'));
@ -125,15 +125,28 @@ export const compileAndTransform = async (
pathInOutput('elm.opt.transformed.min.js')
);
gzip(pathInOutput('elm.opt.transformed.min.js'));
await minify(
pathInOutput('elm.opt.prepack.js'),
pathInOutput('elm.opt.prepack.min.js')
);
gzip(pathInOutput('elm.opt.prepack.min.js'));
return {};
};
function includeObjectUpdate(objectUpdate: ObjectUpdate | null): any {
if (objectUpdate != null) {
return [true, createReplaceUtilsUpdateWithObjectSpread(objectUpdate)];
} else {
return [];
}
}
function removeDisabled(list: any[]) {
let newList: any[] = [];
list.forEach(item => {
if (item != 0 && item[0]) {
newList.push(item[1]);
}
});
return newList;
}
async function minify(inputFilename: string, outputFilename: string) {
const compress = {
toplevel: true,

View File

@ -1,4 +1,5 @@
import ts from 'typescript';
import { ObjectUpdate } from './../types';
const copyWithSpread = `
const _Utils_update = (oldRecord, updatedFields) => {
@ -21,13 +22,8 @@ const extractBody = (sourceText: string): ts.Node => {
return source.statements[0];
};
export enum NativeSpread {
UseSpreadForUpdateAndOriginalRecord = 'for_both',
UseSpreadOnlyToMakeACopy = 'for_copy',
}
export const createReplaceUtilsUpdateWithObjectSpread = (
kind: NativeSpread
kind: ObjectUpdate
): ts.TransformerFactory<ts.SourceFile> => context => {
return sourceFile => {
const visitor = (node: ts.Node): ts.VisitResult<ts.Node> => {
@ -37,9 +33,9 @@ export const createReplaceUtilsUpdateWithObjectSpread = (
node.name?.text === '_Utils_update'
) {
switch (kind) {
case NativeSpread.UseSpreadForUpdateAndOriginalRecord:
case ObjectUpdate.UseSpreadForUpdateAndOriginalRecord:
return extractBody(spreadForBoth);
case NativeSpread.UseSpreadOnlyToMakeACopy:
case ObjectUpdate.UseSpreadOnlyToMakeACopy:
return extractBody(copyWithSpread);
}
}

View File

@ -5,259 +5,35 @@ Compiles all the test cases and runs them via webdriver to summarize the results
*/
import * as compile from './compile-testcases';
import * as webdriver from 'selenium-webdriver';
import * as chrome from 'selenium-webdriver/chrome';
import * as path from 'path';
import * as fs from 'fs';
import { ObjectUpdate } from './types';
import * as Reporting from './reporting';
const visitBenchmark = async (tag: string | null, file: string) => {
let driver = new webdriver.Builder()
.forBrowser('chrome')
// .setChromeOptions(/* ... */)
// .setFirefoxOptions(/* ... */)
.build();
// docs for selenium:
// https://www.selenium.dev/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_WebDriver.html
let result = [];
try {
await driver.get('file://' + path.resolve(file));
await driver.wait(webdriver.until.titleIs('done'), 480000);
result = await driver.executeScript('return window.results;');
} finally {
await driver.quit();
}
return { tag: tag, browser: 'chrome', results: result };
const defaultOptions = {
prepack: true,
variantShapes: true,
inlineFunctions: true,
listLiterals: true,
arrowFns: true,
objectUpdate: ObjectUpdate.UseSpreadOnlyToMakeACopy,
unusedValues: false,
};
export interface Stat {
name: string;
bytes: number;
async function go() {
const report = await Reporting.run([
// { name: 'simple',
// dir: 'testcases/simple',
// elmFile: 'main',
// options: defaultOptions,
// },
{
name: 'bench',
dir: 'testcases/bench',
elmFile: 'Main.elm',
options: defaultOptions,
},
]);
console.log(Reporting.markdown(await report));
}
const assetSizeStats = (dir: string): Stat[] => {
let stats: Stat[] = [];
fs.readdir(dir, function(err, files) {
if (err) {
console.log('Error getting directory information.');
} else {
files.forEach(function(file) {
const stat = fs.statSync(path.join(dir, file));
stats.push({
name: path.basename(file),
bytes: stat.size,
});
});
}
});
return stats;
};
const run = async function() {
await compile.compileAndTransform('testcases/simple', 'Main.elm', {
prepack: true,
});
await compile.compileAndTransform('testcases/bench', 'Main.elm', {
prepack: true,
});
await compile.compileAndTransform('testcases/elm-markup', 'Run.elm', {
prepack: true,
});
await compile.compileAndTransform('testcases/elm-obj-file', 'Run.elm', {
prepack: true,
});
const assets = {
bench: assetSizeStats('testcases/bench/output'),
simple: assetSizeStats('testcases/simple/output'),
elmMarkup: assetSizeStats('testcases/elm-markup/output'),
elmObjFile: assetSizeStats('testcases/elm-obj-file/output'),
};
let results = [];
results.push(await visitBenchmark(null, 'testcases/bench/standard.html'));
results.push(
await visitBenchmark('transformed', 'testcases/bench/transformed.html')
);
results.push(
await visitBenchmark(null, 'testcases/elm-markup/standard.html')
);
results.push(
await visitBenchmark('transformed', 'testcases/elm-markup/transformed.html')
);
results.push(
await visitBenchmark(null, 'testcases/elm-obj-file/standard.html')
);
results.push(
await visitBenchmark(
'transformed',
'testcases/elm-obj-file/transformed.html'
)
);
console.log(markdownNewResults(assets, reformat(results)));
};
const markdownResults = (results: any): string => {
let buffer: string[] = [];
results.forEach((item: any) => {
buffer.push('## ' + item.tag);
buffer.push('*' + item.browser + '*');
buffer.push('');
item.results.forEach((result: any) => {
buffer.push(' **' + result.name + '**');
if (result.status.status == 'success') {
buffer.push(
' ' +
humanizeNumber(result.status.runsPerSecond).padStart(10, ' ') +
' runs/sec (' +
Math.round(result.status.goodnessOfFit * 100) +
'%*)'
);
} else {
buffer.push(' problem running benchmark');
}
console.log(result);
});
buffer.push('');
buffer.push('');
});
buffer.push('');
buffer.push('');
return buffer.join('\n');
};
const markdownNewResults = (
assets: { [key: string]: Stat[] },
results: any
): string => {
let buffer: string[] = [];
buffer.push('# Benchmark results');
buffer.push('');
// List asset sizes
for (let key in assets) {
buffer.push('## ' + key + ' asset overview');
buffer.push('');
assets[key].forEach((item: Stat) => {
buffer.push(
' ' +
item.name.padEnd(40, ' ') +
'' +
humanizeNumber(
roundToDecimal(1, item.bytes / Math.pow(2, 10))
).padStart(10, ' ') +
'kb'
);
});
buffer.push('');
}
buffer.push('');
// List benchmarks
for (let key in results) {
buffer.push('## ' + key);
buffer.push('');
let base: number | null = null;
results[key].forEach((item: any) => {
let tag = '';
if (item.tag != null) {
tag = ', ' + item.tag;
}
if (base == null) {
base = item.status.runsPerSecond;
} else {
let percentChange = (item.status.runsPerSecond / base) * 100;
tag = tag + ' (' + Math.round(percentChange) + '%)';
}
buffer.push(
' ' +
humanizeNumber(item.status.runsPerSecond).padStart(10, ' ') +
' runs/sec (' +
Math.round(item.status.goodnessOfFit * 100) +
'%*), ' +
item.browser +
tag
);
});
buffer.push('');
buffer.push('');
}
buffer.push('');
buffer.push('');
return buffer.join('\n');
};
/*
Current shape
{ browser: 'chrome
, tag: 'transformed'
, results: [
name: 'sum 1000 entities in a list',
status: {
goodnessOfFit: 0.9924404521135742,
runsPerSecond: 72127,
status: 'success'
}
}
{
name: '1000 record updates',
status: {
goodnessOfFit: 0.9955251757469299,
runsPerSecond: 2433,
status: 'success'
}
}
]
NewShape
{ test: 'sum 1000 entities in a list'
, results:
[ { browser: 'chrome'
, tag: 'transformed'
, status: {
goodnessOfFit: 0.9955251757469299,
runsPerSecond: 2433,
status: 'success'
}
}
]
}
*/
function reformat(results: any): any {
let reformed: any = {};
results.forEach((item: any) => {
item.results.forEach((result: any) => {
const newItem = {
browser: item.browser,
tag: item.tag,
status: result.status,
};
if (result.name in reformed) {
reformed[result.name].push(newItem);
} else {
reformed[result.name] = [newItem];
}
});
});
return reformed;
}
// adds commas to the number so its easier to read.
function humanizeNumber(x: number): string {
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
function roundToDecimal(level: number, num: number): number {
let factor: number = Math.pow(10, level);
return Math.round(num * factor) / factor;
}
run();
go();

194
src/reporting.ts Normal file
View File

@ -0,0 +1,194 @@
import * as fs from 'fs';
import * as path from 'path';
import * as Compile from './compile-testcases';
import { Transforms, ObjectUpdate } from './types';
import * as Visit from './visit';
export interface Stat {
name: string;
bytes: number;
}
export const assetSizeStats = (dir: string): Stat[] => {
let stats: Stat[] = [];
fs.readdir(dir, function(err, files) {
if (err) {
console.log('Error getting directory information.');
} else {
files.forEach(function(file) {
const stat = fs.statSync(path.join(dir, file));
stats.push({
name: path.basename(file),
bytes: stat.size,
});
});
}
});
return stats;
};
type Results = {
assets: { [key: string]: Stat[] };
benchmarks: any;
};
export const markdown = (report: Results): string => {
let buffer: string[] = [];
buffer.push('# Benchmark results');
buffer.push('');
// List asset sizes
for (let key in report.assets) {
buffer.push('## ' + key + ' asset overview');
buffer.push('');
report.assets[key].forEach((item: Stat) => {
buffer.push(
' ' +
item.name.padEnd(40, ' ') +
'' +
humanizeNumber(
roundToDecimal(1, item.bytes / Math.pow(2, 10))
).padStart(10, ' ') +
'kb'
);
});
buffer.push('');
}
buffer.push('');
// List benchmarks
for (let key in report.benchmarks) {
buffer.push('## ' + key);
buffer.push('');
let base: number | null = null;
report.benchmarks[key].forEach((item: any) => {
let tag = '';
let delta: string = '';
if (item.tag != null) {
tag = ', ' + item.tag;
}
if (base == null) {
base = item.status.runsPerSecond;
} else {
let percentChange = (item.status.runsPerSecond / base) * 100;
delta = ' (' + Math.round(percentChange) + '%)';
}
const goodness =
'(' + Math.round(item.status.goodnessOfFit * 100) + '%*)';
const label = ' ' + item.browser + tag + goodness;
const datapoint =
humanizeNumber(item.status.runsPerSecond).padStart(10, ' ') +
' runs/sec ' +
delta;
buffer.push(label.padEnd(40, ' ') + datapoint);
});
buffer.push('');
buffer.push('');
}
buffer.push('');
buffer.push('');
return buffer.join('\n');
};
/*
Current shape
{ browser: 'chrome
, tag: 'transformed'
, results: [
name: 'sum 1000 entities in a list',
status: {
goodnessOfFit: 0.9924404521135742,
runsPerSecond: 72127,
status: 'success'
}
}
{
name: '1000 record updates',
status: {
goodnessOfFit: 0.9955251757469299,
runsPerSecond: 2433,
status: 'success'
}
}
]
NewShape
{ test: 'sum 1000 entities in a list'
, results:
[ { browser: 'chrome'
, tag: 'transformed'
, status: {
goodnessOfFit: 0.9955251757469299,
runsPerSecond: 2433,
status: 'success'
}
}
]
}
*/
export function reformat(results: any): any {
let reformed: any = {};
results.forEach((item: any) => {
item.results.forEach((result: any) => {
const newItem = {
browser: item.browser,
tag: item.tag,
status: result.status,
};
if (result.name in reformed) {
reformed[result.name].push(newItem);
} else {
reformed[result.name] = [newItem];
}
});
});
return reformed;
}
// adds commas to the number so its easier to read.
function humanizeNumber(x: number): string {
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
function roundToDecimal(level: number, num: number): number {
let factor: number = Math.pow(10, level);
return Math.round(num * factor) / factor;
}
type Testcase = {
name: string;
dir: string;
elmFile: string;
options: Transforms;
};
export const run = async function(runnable: Testcase[]) {
let results: any[] = [];
let assets: any = {};
for (let instance of runnable) {
await Compile.compileAndTransform(
instance.dir,
instance.elmFile,
instance.options
);
assets[instance.name] = assetSizeStats(path.join(instance.dir, 'output'));
results.push(
await Visit.benchmark(null, path.join(instance.dir, 'standard.html'))
);
results.push(
await Visit.benchmark(
'transformed',
path.join(instance.dir, 'transformed.html')
)
);
}
return { assets: assets, benchmarks: reformat(results) };
};

View File

@ -11,3 +11,18 @@ export interface ElmVariant {
slots: string[];
totalTypeSlotCount: number;
}
export type Transforms = {
prepack: boolean;
variantShapes: boolean;
inlineFunctions: boolean;
listLiterals: boolean;
arrowFns: boolean;
objectUpdate: ObjectUpdate | null;
unusedValues: boolean;
};
export enum ObjectUpdate {
UseSpreadForUpdateAndOriginalRecord = 'for_both',
UseSpreadOnlyToMakeACopy = 'for_copy',
}

23
src/visit.ts Normal file
View File

@ -0,0 +1,23 @@
import * as Webdriver from 'selenium-webdriver';
import * as chrome from 'selenium-webdriver/chrome';
import * as Path from 'path';
export const benchmark = async (tag: string | null, file: string) => {
let driver = new Webdriver.Builder()
.forBrowser('chrome')
// .setChromeOptions(/* ... */)
// .setFirefoxOptions(/* ... */)
.build();
// docs for selenium:
// https://www.selenium.dev/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_WebDriver.html
let result = [];
try {
await driver.get('file://' + Path.resolve(file));
await driver.wait(Webdriver.until.titleIs('done'), 480000);
result = await driver.executeScript('return window.results;');
} finally {
await driver.quit();
}
return { tag: tag, browser: 'chrome', results: result };
};