Skip to content

Commit 0ebe535

Browse files
authored
Merge pull request #1299 from sveltejs/gh-1257
Stats
2 parents acae17d + 304a0e8 commit 0ebe535

File tree

15 files changed

+260
-11
lines changed

15 files changed

+260
-11
lines changed

src/Stats.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { Node, Warning } from './interfaces';
2+
import Generator from './generators/Generator';
3+
4+
const now = (typeof process !== 'undefined' && process.hrtime)
5+
? () => {
6+
const t = process.hrtime();
7+
return t[0] * 1e3 + t[1] / 1e6;
8+
}
9+
: () => window.performance.now();
10+
11+
type Timing = {
12+
label: string;
13+
start: number;
14+
end: number;
15+
children: Timing[];
16+
}
17+
18+
function collapseTimings(timings) {
19+
const result = {};
20+
timings.forEach(timing => {
21+
result[timing.label] = Object.assign({
22+
total: timing.end - timing.start
23+
}, timing.children && collapseTimings(timing.children));
24+
});
25+
return result;
26+
}
27+
28+
export default class Stats {
29+
startTime: number;
30+
currentTiming: Timing;
31+
currentChildren: Timing[];
32+
timings: Timing[];
33+
stack: Timing[];
34+
warnings: Warning[];
35+
36+
constructor() {
37+
this.startTime = now();
38+
this.stack = [];
39+
this.currentChildren = this.timings = [];
40+
41+
this.warnings = [];
42+
}
43+
44+
start(label) {
45+
const timing = {
46+
label,
47+
start: now(),
48+
end: null,
49+
children: []
50+
};
51+
52+
this.currentChildren.push(timing);
53+
this.stack.push(timing);
54+
55+
this.currentTiming = timing;
56+
this.currentChildren = timing.children;
57+
}
58+
59+
stop(label) {
60+
if (label !== this.currentTiming.label) {
61+
throw new Error(`Mismatched timing labels`);
62+
}
63+
64+
this.currentTiming.end = now();
65+
this.stack.pop();
66+
this.currentTiming = this.stack[this.stack.length - 1];
67+
this.currentChildren = this.currentTiming ? this.currentTiming.children : this.timings;
68+
}
69+
70+
render(generator: Generator) {
71+
const timings = Object.assign({
72+
total: now() - this.startTime
73+
}, collapseTimings(this.timings));
74+
75+
const imports = generator.imports.map(node => {
76+
return {
77+
source: node.source.value,
78+
specifiers: node.specifiers.map(specifier => {
79+
return {
80+
name: (
81+
specifier.type === 'ImportDefaultSpecifier' ? 'default' :
82+
specifier.type === 'ImportNamespaceSpecifier' ? '*' :
83+
specifier.imported.name
84+
),
85+
as: specifier.local.name
86+
};
87+
})
88+
}
89+
});
90+
91+
const hooks: Record<string, boolean> = {};
92+
if (generator.templateProperties.oncreate) hooks.oncreate = true;
93+
if (generator.templateProperties.ondestroy) hooks.ondestroy = true;
94+
95+
return {
96+
timings,
97+
warnings: this.warnings,
98+
imports,
99+
hooks
100+
};
101+
}
102+
}

src/generators/Generator.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import MagicString, { Bundle } from 'magic-string';
22
import isReference from 'is-reference';
33
import { walk, childKeys } from 'estree-walker';
44
import { getLocator } from 'locate-character';
5+
import Stats from '../Stats';
56
import deindent from '../utils/deindent';
67
import CodeBuilder from '../utils/CodeBuilder';
78
import getCodeFrame from '../utils/getCodeFrame';
@@ -76,6 +77,8 @@ childKeys.EachBlock = childKeys.IfBlock = ['children', 'else'];
7677
childKeys.Attribute = ['value'];
7778

7879
export default class Generator {
80+
stats: Stats;
81+
7982
ast: Parsed;
8083
parsed: Parsed;
8184
source: string;
@@ -123,8 +126,12 @@ export default class Generator {
123126
name: string,
124127
stylesheet: Stylesheet,
125128
options: CompileOptions,
129+
stats: Stats,
126130
dom: boolean
127131
) {
132+
stats.start('compile');
133+
this.stats = stats;
134+
128135
this.ast = clone(parsed);
129136

130137
this.parsed = parsed;
@@ -372,10 +379,13 @@ export default class Generator {
372379
}
373380
});
374381

382+
this.stats.stop('compile');
383+
375384
return {
376385
ast: this.ast,
377386
js,
378387
css,
388+
stats: this.stats.render(this),
379389

380390
// TODO deprecate
381391
code: js.code,

src/generators/dom/index.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import reservedNames from '../../utils/reservedNames';
1111
import shared from './shared';
1212
import Generator from '../Generator';
1313
import Stylesheet from '../../css/Stylesheet';
14+
import Stats from '../../Stats';
1415
import Block from './Block';
1516
import { test } from '../../config';
1617
import { Parsed, CompileOptions, Node } from '../../interfaces';
@@ -34,9 +35,10 @@ export class DomGenerator extends Generator {
3435
source: string,
3536
name: string,
3637
stylesheet: Stylesheet,
37-
options: CompileOptions
38+
options: CompileOptions,
39+
stats: Stats
3840
) {
39-
super(parsed, source, name, stylesheet, options, true);
41+
super(parsed, source, name, stylesheet, options, stats, true);
4042
this.blocks = [];
4143

4244
this.readonly = new Set();
@@ -54,11 +56,12 @@ export default function dom(
5456
parsed: Parsed,
5557
source: string,
5658
stylesheet: Stylesheet,
57-
options: CompileOptions
59+
options: CompileOptions,
60+
stats: Stats
5861
) {
5962
const format = options.format || 'es';
6063

61-
const generator = new DomGenerator(parsed, source, options.name || 'SvelteComponent', stylesheet, options);
64+
const generator = new DomGenerator(parsed, source, options.name || 'SvelteComponent', stylesheet, options, stats);
6265

6366
const {
6467
computations,

src/generators/server-side-rendering/index.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import deindent from '../../utils/deindent';
22
import Generator from '../Generator';
3+
import Stats from '../../Stats';
34
import Stylesheet from '../../css/Stylesheet';
45
import Block from './Block';
56
import visit from './visit';
@@ -20,9 +21,10 @@ export class SsrGenerator extends Generator {
2021
source: string,
2122
name: string,
2223
stylesheet: Stylesheet,
23-
options: CompileOptions
24+
options: CompileOptions,
25+
stats: Stats
2426
) {
25-
super(parsed, source, name, stylesheet, options, false);
27+
super(parsed, source, name, stylesheet, options, stats, false);
2628
this.bindings = [];
2729
this.renderCode = '';
2830
this.appendTargets = [];
@@ -45,11 +47,12 @@ export default function ssr(
4547
parsed: Parsed,
4648
source: string,
4749
stylesheet: Stylesheet,
48-
options: CompileOptions
50+
options: CompileOptions,
51+
stats: Stats
4952
) {
5053
const format = options.format || 'cjs';
5154

52-
const generator = new SsrGenerator(parsed, source, options.name || 'SvelteComponent', stylesheet, options);
55+
const generator = new SsrGenerator(parsed, source, options.name || 'SvelteComponent', stylesheet, options, stats);
5356

5457
const { computations, name, templateProperties } = generator;
5558

src/index.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import parse from './parse/index';
22
import validate from './validate/index';
33
import generate from './generators/dom/index';
44
import generateSSR from './generators/server-side-rendering/index';
5+
import Stats from './Stats';
56
import { assign } from './shared/index.js';
67
import Stylesheet from './css/Stylesheet';
78
import { Parsed, CompileOptions, Warning, PreprocessOptions, Preprocessor } from './interfaces';
@@ -109,20 +110,36 @@ export function compile(source: string, _options: CompileOptions) {
109110
const options = normalizeOptions(_options);
110111
let parsed: Parsed;
111112

113+
const stats = new Stats();
114+
112115
try {
116+
stats.start('parse');
113117
parsed = parse(source, options);
118+
stats.stop('parse');
114119
} catch (err) {
115120
options.onerror(err);
116121
return;
117122
}
118123

124+
stats.start('stylesheet');
119125
const stylesheet = new Stylesheet(source, parsed, options.filename, options.cascade !== false, options.dev);
126+
stats.stop('stylesheet');
127+
128+
stats.start('validate');
129+
// TODO remove this when we remove svelte.validate from public API — we
130+
// can use the stats object instead
131+
const onwarn = options.onwarn;
132+
options.onwarn = warning => {
133+
stats.warnings.push(warning);
134+
onwarn(warning);
135+
};
120136

121137
validate(parsed, source, stylesheet, options);
138+
stats.stop('validate');
122139

123140
const compiler = options.generate === 'ssr' ? generateSSR : generate;
124141

125-
return compiler(parsed, source, stylesheet, options);
142+
return compiler(parsed, source, stylesheet, options, stats);
126143
};
127144

128145
export function create(source: string, _options: CompileOptions = {}) {

src/interfaces.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,4 +93,4 @@ export interface PreprocessOptions {
9393
filename?: string
9494
}
9595

96-
export type Preprocessor = (options: {content: string, attributes: Record<string, string | boolean>, filename?: string}) => { code: string, map?: SourceMap | string };
96+
export type Preprocessor = (options: {content: string, attributes: Record<string, string | boolean>, filename?: string}) => { code: string, map?: SourceMap | string };

src/validate/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import validateJs from './js/index';
22
import validateHtml from './html/index';
33
import { getLocator, Location } from 'locate-character';
44
import getCodeFrame from '../utils/getCodeFrame';
5+
import Stats from '../Stats';
56
import error from '../utils/error';
67
import Stylesheet from '../css/Stylesheet';
78
import { Node, Parsed, CompileOptions, Warning } from '../interfaces';

test/stats/index.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import * as fs from 'fs';
2+
import assert from 'assert';
3+
import { svelte, loadConfig, tryToLoadJson } from '../helpers.js';
4+
5+
describe('stats', () => {
6+
fs.readdirSync('test/stats/samples').forEach(dir => {
7+
if (dir[0] === '.') return;
8+
9+
// add .solo to a sample directory name to only run that test
10+
const solo = /\.solo/.test(dir);
11+
const skip = /\.skip/.test(dir);
12+
13+
if (solo && process.env.CI) {
14+
throw new Error('Forgot to remove `solo: true` from test');
15+
}
16+
17+
(solo ? it.only : skip ? it.skip : it)(dir, () => {
18+
const config = loadConfig(`./stats/samples/${dir}/_config.js`);
19+
const filename = `test/stats/samples/${dir}/input.html`;
20+
const input = fs.readFileSync(filename, 'utf-8').replace(/\s+$/, '');
21+
22+
const expectedWarnings =
23+
tryToLoadJson(`test/stats/samples/${dir}/warnings.json`) || [];
24+
const expectedError = tryToLoadJson(
25+
`test/stats/samples/${dir}/error.json`
26+
);
27+
28+
let result;
29+
let error;
30+
31+
try {
32+
result = svelte.compile(input, config.options);
33+
} catch (e) {
34+
error = e;
35+
}
36+
37+
config.test(assert, result.stats);
38+
39+
if (result.stats.warnings.length || expectedWarnings.length) {
40+
// TODO check warnings are added to stats.warnings
41+
}
42+
43+
if (error || expectedError) {
44+
if (error && !expectedError) {
45+
throw error;
46+
}
47+
48+
if (expectedError && !error) {
49+
throw new Error(`Expected an error: ${expectedError.message}`);
50+
}
51+
52+
assert.equal(error.message, expectedError.message);
53+
assert.deepEqual(error.loc, expectedError.loc);
54+
assert.deepEqual(error.end, expectedError.end);
55+
assert.equal(error.pos, expectedError.pos);
56+
}
57+
});
58+
});
59+
});

test/stats/samples/basic/_config.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export default {
2+
test(assert, stats) {
3+
assert.equal(typeof stats.timings, 'object');
4+
assert.equal(typeof stats.timings.total, 'number');
5+
}
6+
};

test/stats/samples/basic/input.html

Whitespace-only changes.

test/stats/samples/hooks/_config.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default {
2+
test(assert, stats) {
3+
assert.deepEqual(stats.hooks, {
4+
oncreate: true
5+
});
6+
}
7+
};

test/stats/samples/hooks/input.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script>
2+
export default {
3+
oncreate() {
4+
console.log('creating');
5+
}
6+
};
7+
</script>

test/stats/samples/imports/_config.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export default {
2+
test(assert, stats) {
3+
assert.deepEqual(stats.imports, [
4+
{
5+
source: 'x',
6+
specifiers: [{ name: 'default', as: 'x' }]
7+
},
8+
{
9+
source: 'y',
10+
specifiers: [{ name: 'y', as: 'y' }]
11+
},
12+
{
13+
source: 'z',
14+
specifiers: [{ name: '*', as: 'z' }]
15+
}
16+
]);
17+
}
18+
};

test/stats/samples/imports/input.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<script>
2+
import x from 'x';
3+
import { y } from 'y';
4+
import * as z from 'z';
5+
</script>

0 commit comments

Comments
 (0)