From 75c1fbcf7b04651a660b8b40d048908b071601ce Mon Sep 17 00:00:00 2001 From: Rich-Harris Date: Sun, 1 Apr 2018 13:36:05 -0400 Subject: [PATCH 1/4] add a stats object with timings (#1257) --- src/Stats.ts | 73 +++++++++++++++++++ src/generators/Generator.ts | 10 +++ src/generators/dom/index.ts | 11 ++- src/generators/server-side-rendering/index.ts | 11 ++- src/index.ts | 11 ++- src/interfaces.ts | 2 +- 6 files changed, 108 insertions(+), 10 deletions(-) create mode 100644 src/Stats.ts diff --git a/src/Stats.ts b/src/Stats.ts new file mode 100644 index 000000000000..fd2c45ce0ae7 --- /dev/null +++ b/src/Stats.ts @@ -0,0 +1,73 @@ +const now = (typeof process !== 'undefined' && process.hrtime) + ? () => { + const t = process.hrtime(); + return t[0] * 1e3 + t[1] / 1e6; + } + : () => window.performance.now(); + +type Timing = { + label: string; + start: number; + end: number; + children: Timing[]; +} + +function collapseTimings(timings) { + const result = {}; + timings.forEach(timing => { + result[timing.label] = Object.assign({ + total: timing.end - timing.start + }, timing.children && collapseTimings(timing.children)); + }); + return result; +} + +export default class Stats { + startTime: number; + currentTiming: Timing; + currentChildren: Timing[]; + timings: Timing[]; + stack: Timing[]; + + constructor() { + this.startTime = now(); + this.stack = []; + this.currentChildren = this.timings = []; + } + + start(label) { + const timing = { + label, + start: now(), + end: null, + children: [] + }; + + this.currentChildren.push(timing); + this.stack.push(timing); + + this.currentTiming = timing; + this.currentChildren = timing.children; + } + + stop(label) { + if (label !== this.currentTiming.label) { + throw new Error(`Mismatched timing labels`); + } + + this.currentTiming.end = now(); + this.stack.pop(); + this.currentTiming = this.stack[this.stack.length - 1]; + this.currentChildren = this.currentTiming ? this.currentTiming.children : this.timings; + } + + toJSON() { + const timings = Object.assign({ + total: now() - this.startTime + }, collapseTimings(this.timings)); + + return { + timings + }; + } +} \ No newline at end of file diff --git a/src/generators/Generator.ts b/src/generators/Generator.ts index 5d8e472c5b7a..9a0e9acd4394 100644 --- a/src/generators/Generator.ts +++ b/src/generators/Generator.ts @@ -2,6 +2,7 @@ import MagicString, { Bundle } from 'magic-string'; import isReference from 'is-reference'; import { walk, childKeys } from 'estree-walker'; import { getLocator } from 'locate-character'; +import Stats from '../Stats'; import deindent from '../utils/deindent'; import CodeBuilder from '../utils/CodeBuilder'; import getCodeFrame from '../utils/getCodeFrame'; @@ -76,6 +77,8 @@ childKeys.EachBlock = childKeys.IfBlock = ['children', 'else']; childKeys.Attribute = ['value']; export default class Generator { + stats: Stats; + ast: Parsed; parsed: Parsed; source: string; @@ -123,8 +126,12 @@ export default class Generator { name: string, stylesheet: Stylesheet, options: CompileOptions, + stats: Stats, dom: boolean ) { + stats.start('compile'); + this.stats = stats; + this.ast = clone(parsed); this.parsed = parsed; @@ -372,10 +379,13 @@ export default class Generator { } }); + this.stats.stop('compile'); + return { ast: this.ast, js, css, + stats: this.stats.toJSON(), // TODO deprecate code: js.code, diff --git a/src/generators/dom/index.ts b/src/generators/dom/index.ts index c3358f31d32a..412742d87478 100644 --- a/src/generators/dom/index.ts +++ b/src/generators/dom/index.ts @@ -11,6 +11,7 @@ import reservedNames from '../../utils/reservedNames'; import shared from './shared'; import Generator from '../Generator'; import Stylesheet from '../../css/Stylesheet'; +import Stats from '../../Stats'; import Block from './Block'; import { test } from '../../config'; import { Parsed, CompileOptions, Node } from '../../interfaces'; @@ -34,9 +35,10 @@ export class DomGenerator extends Generator { source: string, name: string, stylesheet: Stylesheet, - options: CompileOptions + options: CompileOptions, + stats: Stats ) { - super(parsed, source, name, stylesheet, options, true); + super(parsed, source, name, stylesheet, options, stats, true); this.blocks = []; this.readonly = new Set(); @@ -81,11 +83,12 @@ export default function dom( parsed: Parsed, source: string, stylesheet: Stylesheet, - options: CompileOptions + options: CompileOptions, + stats: Stats ) { const format = options.format || 'es'; - const generator = new DomGenerator(parsed, source, options.name || 'SvelteComponent', stylesheet, options); + const generator = new DomGenerator(parsed, source, options.name || 'SvelteComponent', stylesheet, options, stats); const { computations, diff --git a/src/generators/server-side-rendering/index.ts b/src/generators/server-side-rendering/index.ts index a4e365235942..e2501a826d0a 100644 --- a/src/generators/server-side-rendering/index.ts +++ b/src/generators/server-side-rendering/index.ts @@ -1,5 +1,6 @@ import deindent from '../../utils/deindent'; import Generator from '../Generator'; +import Stats from '../../Stats'; import Stylesheet from '../../css/Stylesheet'; import Block from './Block'; import visit from './visit'; @@ -20,9 +21,10 @@ export class SsrGenerator extends Generator { source: string, name: string, stylesheet: Stylesheet, - options: CompileOptions + options: CompileOptions, + stats: Stats ) { - super(parsed, source, name, stylesheet, options, false); + super(parsed, source, name, stylesheet, options, stats, false); this.bindings = []; this.renderCode = ''; this.appendTargets = []; @@ -45,11 +47,12 @@ export default function ssr( parsed: Parsed, source: string, stylesheet: Stylesheet, - options: CompileOptions + options: CompileOptions, + stats: Stats ) { const format = options.format || 'cjs'; - const generator = new SsrGenerator(parsed, source, options.name || 'SvelteComponent', stylesheet, options); + const generator = new SsrGenerator(parsed, source, options.name || 'SvelteComponent', stylesheet, options, stats); const { computations, name, templateProperties } = generator; diff --git a/src/index.ts b/src/index.ts index cc3c0681e58c..3167498f85b7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,7 @@ import parse from './parse/index'; import validate from './validate/index'; import generate from './generators/dom/index'; import generateSSR from './generators/server-side-rendering/index'; +import Stats from './Stats'; import { assign } from './shared/index.js'; import Stylesheet from './css/Stylesheet'; import { Parsed, CompileOptions, Warning, PreprocessOptions, Preprocessor } from './interfaces'; @@ -109,20 +110,28 @@ export function compile(source: string, _options: CompileOptions) { const options = normalizeOptions(_options); let parsed: Parsed; + const stats = new Stats(); + try { + stats.start('parse'); parsed = parse(source, options); + stats.stop('parse'); } catch (err) { options.onerror(err); return; } + stats.start('stylesheet'); const stylesheet = new Stylesheet(source, parsed, options.filename, options.cascade !== false, options.dev); + stats.stop('stylesheet'); + stats.start('validate'); validate(parsed, source, stylesheet, options); + stats.stop('validate'); const compiler = options.generate === 'ssr' ? generateSSR : generate; - return compiler(parsed, source, stylesheet, options); + return compiler(parsed, source, stylesheet, options, stats); }; export function create(source: string, _options: CompileOptions = {}) { diff --git a/src/interfaces.ts b/src/interfaces.ts index 07692991bb76..d44c6a030c5d 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -93,4 +93,4 @@ export interface PreprocessOptions { filename?: string } -export type Preprocessor = (options: {content: string, attributes: Record, filename?: string}) => { code: string, map?: SourceMap | string }; +export type Preprocessor = (options: {content: string, attributes: Record, filename?: string}) => { code: string, map?: SourceMap | string }; \ No newline at end of file From 788aa89b41380f13988a59e6dd6ccf4e3c4e26b9 Mon Sep 17 00:00:00 2001 From: Rich-Harris Date: Sun, 1 Apr 2018 14:00:35 -0400 Subject: [PATCH 2/4] add stats tests, handle imports --- src/Stats.ts | 24 ++++++++++- src/generators/Generator.ts | 4 +- test/stats/index.js | 59 +++++++++++++++++++++++++++ test/stats/samples/basic/_config.js | 6 +++ test/stats/samples/basic/input.html | 0 test/stats/samples/imports/_config.js | 18 ++++++++ test/stats/samples/imports/input.html | 5 +++ 7 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 test/stats/index.js create mode 100644 test/stats/samples/basic/_config.js create mode 100644 test/stats/samples/basic/input.html create mode 100644 test/stats/samples/imports/_config.js create mode 100644 test/stats/samples/imports/input.html diff --git a/src/Stats.ts b/src/Stats.ts index fd2c45ce0ae7..7994d9b04834 100644 --- a/src/Stats.ts +++ b/src/Stats.ts @@ -1,3 +1,5 @@ +import { Node } from './interfaces'; + const now = (typeof process !== 'undefined' && process.hrtime) ? () => { const t = process.hrtime(); @@ -61,13 +63,31 @@ export default class Stats { this.currentChildren = this.currentTiming ? this.currentTiming.children : this.timings; } - toJSON() { + render({ imports }: { + imports: Node[] + }) { const timings = Object.assign({ total: now() - this.startTime }, collapseTimings(this.timings)); return { - timings + timings, + warnings: [], // TODO + imports: imports.map(node => { + return { + source: node.source.value, + specifiers: node.specifiers.map(specifier => { + return { + name: ( + specifier.type === 'ImportDefaultSpecifier' ? 'default' : + specifier.type === 'ImportNamespaceSpecifier' ? '*' : + specifier.imported.name + ), + as: specifier.local.name + }; + }) + } + }) }; } } \ No newline at end of file diff --git a/src/generators/Generator.ts b/src/generators/Generator.ts index 9a0e9acd4394..cf423739d40e 100644 --- a/src/generators/Generator.ts +++ b/src/generators/Generator.ts @@ -385,7 +385,9 @@ export default class Generator { ast: this.ast, js, css, - stats: this.stats.toJSON(), + stats: this.stats.render({ + imports: this.imports + }), // TODO deprecate code: js.code, diff --git a/test/stats/index.js b/test/stats/index.js new file mode 100644 index 000000000000..7ff64f0fe029 --- /dev/null +++ b/test/stats/index.js @@ -0,0 +1,59 @@ +import * as fs from 'fs'; +import assert from 'assert'; +import { svelte, loadConfig, tryToLoadJson } from '../helpers.js'; + +describe('stats', () => { + fs.readdirSync('test/stats/samples').forEach(dir => { + if (dir[0] === '.') return; + + // add .solo to a sample directory name to only run that test + const solo = /\.solo/.test(dir); + const skip = /\.skip/.test(dir); + + if (solo && process.env.CI) { + throw new Error('Forgot to remove `solo: true` from test'); + } + + (solo ? it.only : skip ? it.skip : it)(dir, () => { + const config = loadConfig(`./stats/samples/${dir}/_config.js`); + const filename = `test/stats/samples/${dir}/input.html`; + const input = fs.readFileSync(filename, 'utf-8').replace(/\s+$/, ''); + + const expectedWarnings = + tryToLoadJson(`test/stats/samples/${dir}/warnings.json`) || []; + const expectedError = tryToLoadJson( + `test/stats/samples/${dir}/error.json` + ); + + let result; + let error; + + try { + result = svelte.compile(input, config.options); + } catch (e) { + error = e; + } + + config.test(assert, result.stats); + + if (result.stats.warnings.length || expectedWarnings.length) { + // TODO check warnings are added to stats.warnings + } + + if (error || expectedError) { + if (error && !expectedError) { + throw error; + } + + if (expectedError && !error) { + throw new Error(`Expected an error: ${expectedError.message}`); + } + + assert.equal(error.message, expectedError.message); + assert.deepEqual(error.loc, expectedError.loc); + assert.deepEqual(error.end, expectedError.end); + assert.equal(error.pos, expectedError.pos); + } + }); + }); +}); diff --git a/test/stats/samples/basic/_config.js b/test/stats/samples/basic/_config.js new file mode 100644 index 000000000000..434e11bd7643 --- /dev/null +++ b/test/stats/samples/basic/_config.js @@ -0,0 +1,6 @@ +export default { + test(assert, stats) { + assert.equal(typeof stats.timings, 'object'); + assert.equal(typeof stats.timings.total, 'number'); + } +}; \ No newline at end of file diff --git a/test/stats/samples/basic/input.html b/test/stats/samples/basic/input.html new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/stats/samples/imports/_config.js b/test/stats/samples/imports/_config.js new file mode 100644 index 000000000000..f7c15183dd2a --- /dev/null +++ b/test/stats/samples/imports/_config.js @@ -0,0 +1,18 @@ +export default { + test(assert, stats) { + assert.deepEqual(stats.imports, [ + { + source: 'x', + specifiers: [{ name: 'default', as: 'x' }] + }, + { + source: 'y', + specifiers: [{ name: 'y', as: 'y' }] + }, + { + source: 'z', + specifiers: [{ name: '*', as: 'z' }] + } + ]); + } +}; \ No newline at end of file diff --git a/test/stats/samples/imports/input.html b/test/stats/samples/imports/input.html new file mode 100644 index 000000000000..a7b59691ac99 --- /dev/null +++ b/test/stats/samples/imports/input.html @@ -0,0 +1,5 @@ + \ No newline at end of file From a5d5a6c95238329d5b18462b07ef99401e281a3b Mon Sep 17 00:00:00 2001 From: Rich-Harris Date: Sun, 1 Apr 2018 14:10:03 -0400 Subject: [PATCH 3/4] add lifecycle hooks --- src/Stats.ts | 42 ++++++++++++++++------------- src/generators/Generator.ts | 4 +-- test/stats/samples/hooks/_config.js | 7 +++++ test/stats/samples/hooks/input.html | 7 +++++ 4 files changed, 39 insertions(+), 21 deletions(-) create mode 100644 test/stats/samples/hooks/_config.js create mode 100644 test/stats/samples/hooks/input.html diff --git a/src/Stats.ts b/src/Stats.ts index 7994d9b04834..5f8c7d4f94d5 100644 --- a/src/Stats.ts +++ b/src/Stats.ts @@ -1,4 +1,5 @@ import { Node } from './interfaces'; +import Generator from './generators/Generator'; const now = (typeof process !== 'undefined' && process.hrtime) ? () => { @@ -63,31 +64,36 @@ export default class Stats { this.currentChildren = this.currentTiming ? this.currentTiming.children : this.timings; } - render({ imports }: { - imports: Node[] - }) { + render(generator: Generator) { const timings = Object.assign({ total: now() - this.startTime }, collapseTimings(this.timings)); + const imports = generator.imports.map(node => { + return { + source: node.source.value, + specifiers: node.specifiers.map(specifier => { + return { + name: ( + specifier.type === 'ImportDefaultSpecifier' ? 'default' : + specifier.type === 'ImportNamespaceSpecifier' ? '*' : + specifier.imported.name + ), + as: specifier.local.name + }; + }) + } + }); + + const hooks: Record = {}; + if (generator.templateProperties.oncreate) hooks.oncreate = true; + if (generator.templateProperties.ondestroy) hooks.ondestroy = true; + return { timings, warnings: [], // TODO - imports: imports.map(node => { - return { - source: node.source.value, - specifiers: node.specifiers.map(specifier => { - return { - name: ( - specifier.type === 'ImportDefaultSpecifier' ? 'default' : - specifier.type === 'ImportNamespaceSpecifier' ? '*' : - specifier.imported.name - ), - as: specifier.local.name - }; - }) - } - }) + imports, + hooks }; } } \ No newline at end of file diff --git a/src/generators/Generator.ts b/src/generators/Generator.ts index cf423739d40e..250255ef9831 100644 --- a/src/generators/Generator.ts +++ b/src/generators/Generator.ts @@ -385,9 +385,7 @@ export default class Generator { ast: this.ast, js, css, - stats: this.stats.render({ - imports: this.imports - }), + stats: this.stats.render(this), // TODO deprecate code: js.code, diff --git a/test/stats/samples/hooks/_config.js b/test/stats/samples/hooks/_config.js new file mode 100644 index 000000000000..78fc771a0638 --- /dev/null +++ b/test/stats/samples/hooks/_config.js @@ -0,0 +1,7 @@ +export default { + test(assert, stats) { + assert.deepEqual(stats.hooks, { + oncreate: true + }); + } +}; \ No newline at end of file diff --git a/test/stats/samples/hooks/input.html b/test/stats/samples/hooks/input.html new file mode 100644 index 000000000000..fde68977b2d2 --- /dev/null +++ b/test/stats/samples/hooks/input.html @@ -0,0 +1,7 @@ + \ No newline at end of file From c0287f208046fdd7f9b92122e179e5c68a73905d Mon Sep 17 00:00:00 2001 From: Rich-Harris Date: Sun, 1 Apr 2018 14:25:33 -0400 Subject: [PATCH 4/4] include warnings in stats object --- src/Stats.ts | 7 +++++-- src/index.ts | 8 ++++++++ src/validate/index.ts | 1 + test/validator/index.js | 13 ++++++++++++- 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/Stats.ts b/src/Stats.ts index 5f8c7d4f94d5..873ae15b460e 100644 --- a/src/Stats.ts +++ b/src/Stats.ts @@ -1,4 +1,4 @@ -import { Node } from './interfaces'; +import { Node, Warning } from './interfaces'; import Generator from './generators/Generator'; const now = (typeof process !== 'undefined' && process.hrtime) @@ -31,11 +31,14 @@ export default class Stats { currentChildren: Timing[]; timings: Timing[]; stack: Timing[]; + warnings: Warning[]; constructor() { this.startTime = now(); this.stack = []; this.currentChildren = this.timings = []; + + this.warnings = []; } start(label) { @@ -91,7 +94,7 @@ export default class Stats { return { timings, - warnings: [], // TODO + warnings: this.warnings, imports, hooks }; diff --git a/src/index.ts b/src/index.ts index 3167498f85b7..16081ea8b4d3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -126,6 +126,14 @@ export function compile(source: string, _options: CompileOptions) { stats.stop('stylesheet'); stats.start('validate'); + // TODO remove this when we remove svelte.validate from public API — we + // can use the stats object instead + const onwarn = options.onwarn; + options.onwarn = warning => { + stats.warnings.push(warning); + onwarn(warning); + }; + validate(parsed, source, stylesheet, options); stats.stop('validate'); diff --git a/src/validate/index.ts b/src/validate/index.ts index fd487ccb0ff2..96e10d9ae82d 100644 --- a/src/validate/index.ts +++ b/src/validate/index.ts @@ -3,6 +3,7 @@ import validateHtml from './html/index'; import { getLocator, Location } from 'locate-character'; import getCodeFrame from '../utils/getCodeFrame'; import CompileError from '../utils/CompileError'; +import Stats from '../Stats'; import Stylesheet from '../css/Stylesheet'; import { Node, Parsed, CompileOptions, Warning } from '../interfaces'; diff --git a/test/validator/index.js b/test/validator/index.js index 4961e712651d..ddfbfb8a4add 100644 --- a/test/validator/index.js +++ b/test/validator/index.js @@ -26,7 +26,7 @@ describe("validate", () => { try { const warnings = []; - svelte.compile(input, { + const { stats } = svelte.compile(input, { onwarn(warning) { warnings.push({ message: warning.message, @@ -38,6 +38,17 @@ describe("validate", () => { dev: config.dev }); + assert.equal(stats.warnings.length, warnings.length); + stats.warnings.forEach((full, i) => { + const lite = warnings[i]; + assert.deepEqual({ + message: full.message, + pos: full.pos, + loc: full.loc, + end: full.end + }, lite); + }); + assert.deepEqual(warnings, expectedWarnings); } catch (e) { error = e;