pw-repl 0.12.4 → 0.12.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +2 -1
- package/README.md +8 -5
- package/bin/pw-repl.js +3 -1
- package/lib/commands.js +4 -0
- package/lib/elements.js +7 -2
- package/lib/help.js +80 -9
- package/lib/inspect.js +12 -5
- package/lib/modes.js +6 -0
- package/lib/output.js +13 -4
- package/lib/profile.js +319 -0
- package/lib/runner.js +2 -2
- package/lib/send.js +3 -2
- package/lib/tabs.js +1 -1
- package/lib/trace.js +530 -0
- package/lib/util.js +4 -1
- package/package.json +1 -1
- package/skill/SKILL.md +6 -1
package/lib/profile.js
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
// Profiling a tab: a Chrome performance trace of what its main thread did, summed up when it stops.
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { state, onShutdown } = require('./state');
|
|
6
|
+
const out = require('./output');
|
|
7
|
+
const { NO_TAB, clock, hints } = require('./util');
|
|
8
|
+
const { tabState } = require('./tabstate');
|
|
9
|
+
const { targetIdOf } = require('./tabs');
|
|
10
|
+
const { summarize, LONG_TASK } = require('./trace');
|
|
11
|
+
|
|
12
|
+
const MAX_SECONDS = 120;
|
|
13
|
+
const SHOWN = 10;
|
|
14
|
+
// DevTools' Performance panel's categories, less the browser-wide ones the summary does not use.
|
|
15
|
+
const CATEGORIES = ['devtools.timeline', 'disabled-by-default-devtools.timeline', 'disabled-by-default-devtools.timeline.frame',
|
|
16
|
+
'disabled-by-default-devtools.timeline.stack', 'toplevel', 'v8.execute', 'blink.user_timing', 'loading', 'latencyInfo',
|
|
17
|
+
// JS samples, which name the function a long task spent its time in; --cpu samples more often.
|
|
18
|
+
'disabled-by-default-v8.cpu_profiler'];
|
|
19
|
+
const CPU_CATEGORIES = ['disabled-by-default-v8.cpu_profiler.hires'];
|
|
20
|
+
|
|
21
|
+
let prof = null;
|
|
22
|
+
// A profile still being read after it ended by itself (its tab closed, or it reached its limit).
|
|
23
|
+
let finishing = null;
|
|
24
|
+
let last = null;
|
|
25
|
+
let dir = null;
|
|
26
|
+
|
|
27
|
+
function currentProfile() {
|
|
28
|
+
return prof;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function workDir() {
|
|
32
|
+
if (!dir) dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pw-repl-profile-'));
|
|
33
|
+
return dir;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// A trace event naming the tab's frame, from its renderer, so an idle tab is still found in the trace.
|
|
37
|
+
// console.timeStamp prints nothing in the console. A page held by a dialog does not answer: no wait.
|
|
38
|
+
// At the end it waits two frames and 100ms more first: a click's timing reaches the trace only after the
|
|
39
|
+
// paint that follows it, and its step takes in what the page did for 100ms after it.
|
|
40
|
+
async function mark(page, { last = false } = {}) {
|
|
41
|
+
if (page.isClosed()) return;
|
|
42
|
+
const marked = page.evaluate(async settle => {
|
|
43
|
+
if (settle) await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(() => setTimeout(resolve, 100))));
|
|
44
|
+
console.timeStamp('pw-repl profile');
|
|
45
|
+
}, last).catch(() => {});
|
|
46
|
+
await Promise.race([marked, new Promise(resolve => setTimeout(resolve, 1000))]);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function forget(profile) {
|
|
50
|
+
if (profile?.saveFile) fs.rmSync(profile.saveFile, { force: true });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function startProfile(tokens, quiet = false) {
|
|
54
|
+
if (prof) throw new Error(`Already profiling ${prof.page.url()}; profile off stops it`);
|
|
55
|
+
const usage = `Usage: profile on [seconds] [--cpu] (seconds from 1 to ${MAX_SECONDS})`;
|
|
56
|
+
const cpu = tokens.includes('--cpu');
|
|
57
|
+
const rest = tokens.filter(t => t !== '--cpu');
|
|
58
|
+
if (rest.length > 1 || (rest.length && !/^\d+$/.test(rest[0]))) throw new Error(usage);
|
|
59
|
+
const seconds = rest.length ? Number(rest[0]) : null;
|
|
60
|
+
if (seconds !== null && (seconds < 1 || seconds > MAX_SECONDS)) throw new Error(usage);
|
|
61
|
+
if (!state.page || state.page.isClosed()) throw new Error(NO_TAB);
|
|
62
|
+
await finishing;
|
|
63
|
+
const page = state.page;
|
|
64
|
+
const frameId = await targetIdOf(page.context(), page);
|
|
65
|
+
if (!frameId) throw new Error('Could not tell which tab this is in the browser; profile needs a Chromium tab');
|
|
66
|
+
// The browser's own session, not the tab's: a tab's session goes when the tab closes, and its trace
|
|
67
|
+
// with it. Chrome ends the trace if the session goes, so a REPL that dies leaves none running.
|
|
68
|
+
const session = await state.browser.newBrowserCDPSession();
|
|
69
|
+
try {
|
|
70
|
+
await session.send('Tracing.start', {
|
|
71
|
+
transferMode: 'ReturnAsStream', streamFormat: 'json', streamCompression: 'none', bufferUsageReportingInterval: 500,
|
|
72
|
+
traceConfig: { includedCategories: cpu ? [...CATEGORIES, ...CPU_CATEGORIES] : CATEGORIES, excludedCategories: ['*'] },
|
|
73
|
+
});
|
|
74
|
+
} catch (error) {
|
|
75
|
+
await session.detach().catch(() => {});
|
|
76
|
+
if (/already/i.test(error.message)) throw new Error('The browser is already recording a trace, and records one at a time: DevTools\' Performance panel, or another REPL\'s profile. Stop that one first.');
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
const profile = { page, session, frameId, cpu, owner: state.client, startedAt: Date.now(), url: page.url(), wake: null };
|
|
80
|
+
prof = profile;
|
|
81
|
+
await mark(page);
|
|
82
|
+
// A profile that stops by itself says why in its summary, and in a notice unless a timed profile on
|
|
83
|
+
// is waiting to print it.
|
|
84
|
+
const stop = (why, notice) => {
|
|
85
|
+
profile.stopped = why;
|
|
86
|
+
finishProfile().then(() => { if (!profile.wake) out.notice(`${notice}; profile shows its summary.`, page); }, () => {});
|
|
87
|
+
};
|
|
88
|
+
const onClose = () => {
|
|
89
|
+
if (prof === profile) stop('the tab closed.', 'The profiled tab closed, which ended the profile');
|
|
90
|
+
};
|
|
91
|
+
page.on('close', onClose);
|
|
92
|
+
// Chrome stops recording when its trace buffer fills, as a busy page does in under a minute; the
|
|
93
|
+
// profile stops with it, so it does not run on recording nothing.
|
|
94
|
+
const onUsage = ({ percentFull }) => {
|
|
95
|
+
if (prof !== profile || !(percentFull >= 0.99)) return;
|
|
96
|
+
const after = Math.round((Date.now() - profile.startedAt) / 1000);
|
|
97
|
+
stop(`Chrome's trace buffer filled after ${after}s. Profile a shorter stretch to have all of it.`, `Chrome's trace buffer filled, which stopped the profile after ${after}s`);
|
|
98
|
+
};
|
|
99
|
+
session.on('Tracing.bufferUsage', onUsage);
|
|
100
|
+
profile.unlisten = () => { page.off('close', onClose); session.off('Tracing.bufferUsage', onUsage); };
|
|
101
|
+
profile.cap = setTimeout(() => {
|
|
102
|
+
if (prof === profile) stop(`it reached ${MAX_SECONDS}s, the most a profile records.`, `The profile stopped at ${MAX_SECONDS}s, the most it records`);
|
|
103
|
+
}, MAX_SECONDS * 1000);
|
|
104
|
+
if (!seconds) {
|
|
105
|
+
if (quiet) return;
|
|
106
|
+
out.log(`Profiling the selected tab${cpu ? ' with a JS CPU profile' : ''}; profile off stops it and prints a summary.`);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
out.log(`Profiling the selected tab for ${seconds}s...`);
|
|
110
|
+
await new Promise(resolve => {
|
|
111
|
+
const timer = setTimeout(resolve, seconds * 1000);
|
|
112
|
+
profile.wake = () => { clearTimeout(timer); resolve(); };
|
|
113
|
+
});
|
|
114
|
+
// profile off may have ended it first, and printed it; anything else that ended it (its tab closed,
|
|
115
|
+
// the buffer filled) leaves it to this command to print.
|
|
116
|
+
if (profile.stoppedByOff) return;
|
|
117
|
+
await finishProfile();
|
|
118
|
+
await profile.finished;
|
|
119
|
+
printProfile(last);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Ends the trace, reads it from the browser, and sums it up. Once, however many ask.
|
|
123
|
+
function finishProfile() {
|
|
124
|
+
const profile = prof;
|
|
125
|
+
if (!profile) return Promise.resolve();
|
|
126
|
+
prof = null;
|
|
127
|
+
clearTimeout(profile.cap);
|
|
128
|
+
profile.unlisten();
|
|
129
|
+
profile.finished = (async () => {
|
|
130
|
+
await mark(profile.page, { last: true });
|
|
131
|
+
const endedAt = Date.now();
|
|
132
|
+
const raw = path.join(workDir(), `raw-${process.pid}-${endedAt}.json`);
|
|
133
|
+
try {
|
|
134
|
+
const complete = new Promise(resolve => profile.session.once('Tracing.tracingComplete', resolve));
|
|
135
|
+
await profile.session.send('Tracing.end');
|
|
136
|
+
const { stream } = await complete;
|
|
137
|
+
const file = fs.openSync(raw, 'w', 0o600);
|
|
138
|
+
try {
|
|
139
|
+
for (;;) {
|
|
140
|
+
const chunk = await profile.session.send('IO.read', { handle: stream, size: 1 << 20 });
|
|
141
|
+
fs.writeSync(file, chunk.base64Encoded ? Buffer.from(chunk.data, 'base64') : chunk.data);
|
|
142
|
+
if (chunk.eof) break;
|
|
143
|
+
}
|
|
144
|
+
} finally {
|
|
145
|
+
fs.closeSync(file);
|
|
146
|
+
await profile.session.send('IO.close', { handle: stream }).catch(() => {});
|
|
147
|
+
}
|
|
148
|
+
await profile.session.detach().catch(() => {});
|
|
149
|
+
forget(last);
|
|
150
|
+
const saveFile = path.join(workDir(), `tab-${process.pid}-${endedAt}.json`);
|
|
151
|
+
const summary = await summarize(raw, { frameId: profile.frameId, url: profile.url, saveFile, cpu: profile.cpu });
|
|
152
|
+
last = { ...profile, endedAt, summary, saveFile, drawing: 'drawing' in tabState(profile.page) };
|
|
153
|
+
} catch (error) {
|
|
154
|
+
await profile.session.detach().catch(() => {});
|
|
155
|
+
last = { ...profile, endedAt, summary: null, error: error.message };
|
|
156
|
+
} finally {
|
|
157
|
+
fs.rmSync(raw, { force: true });
|
|
158
|
+
if (profile.wake) profile.wake();
|
|
159
|
+
}
|
|
160
|
+
})();
|
|
161
|
+
finishing = profile.finished;
|
|
162
|
+
return profile.finished;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function secs(us) {
|
|
166
|
+
return `${(us / 1e6).toFixed(2)}s`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// A total in the unit of the items under it: ms under a second.
|
|
170
|
+
function total(us) {
|
|
171
|
+
return us < 1e6 ? ms(us) : secs(us);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function ms(us) {
|
|
175
|
+
return `${Math.round(us / 1000)}ms`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// The tab's request with this URL nearest to when the trace saw it, for its number in requests.
|
|
179
|
+
function requestNumber(profile, request) {
|
|
180
|
+
const at = profile.startedAt + (request.sent - profile.summary.t0) / 1000;
|
|
181
|
+
const entries = (tabState(profile.page).log || []).filter(e => e.url === request.url);
|
|
182
|
+
const nearest = entries.sort((a, b) => Math.abs(a.t - at) - Math.abs(b.t - at))[0];
|
|
183
|
+
return nearest && Math.abs(nearest.t - at) < 2000 ? `#${nearest.id} ` : '';
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function printProfile(profile, all = false) {
|
|
187
|
+
// So profile off does not show again what has been shown.
|
|
188
|
+
if (finishing && profile === last) finishing.shown = true;
|
|
189
|
+
if (profile.error) { out.log(`The profile from ${clock(profile.startedAt)} could not be read: ${profile.error}`); return; }
|
|
190
|
+
const s = profile.summary;
|
|
191
|
+
const lines = [];
|
|
192
|
+
if (!s) {
|
|
193
|
+
out.log(`Nothing from ${profile.url} was in the trace: its page did no work while it ran, or it closed first.`);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const at = ts => `+${((ts - s.t0) / 1e6).toFixed(2)}s`;
|
|
197
|
+
lines.push(`Profiled ${profile.url} for ${secs(s.duration)}, from ${clock(profile.startedAt)}${profile.cpu ? ', with a JS CPU profile' : ''}`);
|
|
198
|
+
if (profile.stopped) lines.push(`Stopped early: ${profile.stopped}`);
|
|
199
|
+
const split = Object.entries(s.buckets).filter(([, us]) => us >= 5000).map(([b, us]) => `${b} ${secs(us)}`);
|
|
200
|
+
lines.push(`Main thread busy ${secs(s.busy)} (${s.duration ? Math.round((100 * s.busy) / s.duration) : 0}%)${split.length ? `: ${split.join(', ')}` : ''}`);
|
|
201
|
+
if (s.marks.length) lines.push(`Page: ${s.marks.map(([name, ts]) => `${name} ${at(ts)}`).join(', ')}`);
|
|
202
|
+
const longTotal = s.longTasks.reduce((sum, t) => sum + t.dur, 0);
|
|
203
|
+
lines.push(`Long tasks (over ${LONG_TASK / 1000}ms): ${s.longTasks.length ? `${s.longTasks.length}, ${total(longTotal)} in all` : 'none'}`);
|
|
204
|
+
const longest = all ? s.longTasks : [...s.longTasks].sort((a, b) => b.dur - a.dur).slice(0, SHOWN).sort((a, b) => a.ts - b.ts);
|
|
205
|
+
for (const t of longest) {
|
|
206
|
+
const after = t.after ? ` (after ${requestNumber(profile, t.after)}${t.after.method} ${t.after.url}, ${Math.round(t.after.size / 1024)}KB, finished ${at(t.after.finished)})` : '';
|
|
207
|
+
const hot = t.hot ? `; most in ${t.hot.where} (${ms(t.hot.us)})` : '';
|
|
208
|
+
lines.push(` ${at(t.ts).padStart(8)} ${ms(t.dur).padStart(6)} ${[t.kind, t.trigger, t.script].filter(Boolean).join(', ')}${hot}${after}`);
|
|
209
|
+
}
|
|
210
|
+
if (longest.length < s.longTasks.length) lines.push(` (the ${longest.length} longest; profile --all lists them all)`);
|
|
211
|
+
if (s.forced.length) {
|
|
212
|
+
const count = s.forced.reduce((sum, f) => sum + f.count, 0);
|
|
213
|
+
lines.push(`Forced style and layout (work a script waited for, reading a size or style): ${count}, ${ms(s.forced.reduce((sum, f) => sum + f.dur, 0))} in all`);
|
|
214
|
+
for (const f of all ? s.forced : s.forced.slice(0, SHOWN)) lines.push(` ${`${f.count}x`.padStart(5)} ${ms(f.dur).padStart(6)} ${f.where}`);
|
|
215
|
+
}
|
|
216
|
+
if (s.steps.length) {
|
|
217
|
+
lines.push('Steps (input that reached the page, and what it did until 100ms after each step\'s last event):');
|
|
218
|
+
const steps = all ? s.steps : s.steps.slice(0, SHOWN * 2);
|
|
219
|
+
for (const step of steps) {
|
|
220
|
+
const span = step.latency != null
|
|
221
|
+
? `${Math.round(step.latency)}ms to next paint (input delay ${Math.round(step.delay)}ms, handlers ${Math.round(step.handlers)}ms, presentation ${Math.round(step.presentation)}ms)${step.scrolled ? ', the page scrolled' : ''}`
|
|
222
|
+
: `${secs(step.end - step.start)}, ${step.events} event${step.events === 1 ? '' : 's'}`;
|
|
223
|
+
const frames = step.frames ? `, frames ${step.frames} drawn, ${step.dropped} dropped` : '';
|
|
224
|
+
// One under way at profile on (an action still running): what ran before it began is not in the
|
|
225
|
+
// trace, so its busy time would be only the end of it.
|
|
226
|
+
const busy = step.start < s.t0 ? 'began before the profile: its busy time was not recorded' : `busy ${ms(step.busy)}${step.longest ? `, longest task ${ms(step.longest)}` : ''}`;
|
|
227
|
+
lines.push(` ${(step.start < s.t0 ? 'before' : at(step.start)).padStart(8)} ${step.kind} ${span}; ${busy}${frames}`);
|
|
228
|
+
}
|
|
229
|
+
if (steps.length < s.steps.length) lines.push(` (the first ${steps.length} of ${s.steps.length}; profile --all lists them all)`);
|
|
230
|
+
if (profile.drawing && s.steps.some(step => step.frames)) {
|
|
231
|
+
lines.push(' Frames are counted as for a tab in front: pw-repl keeps every tab it uses drawing as if it were. Behind another tab, as the user would have it, Chrome draws far less.');
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (s.cpu) {
|
|
235
|
+
lines.push(s.cpu.length ? 'JS CPU profile, by own time:' : 'JS CPU profile: no JS ran');
|
|
236
|
+
for (const f of all ? s.cpu : s.cpu.slice(0, SHOWN)) lines.push(` ${ms(f.us).padStart(7)} ${f.where}`);
|
|
237
|
+
}
|
|
238
|
+
out.printOutput(lines.join('\n'), true);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const commands = {
|
|
242
|
+
async profile(args, all) {
|
|
243
|
+
const [sub, ...rest] = (args || '').trim().split(/\s+/).filter(Boolean);
|
|
244
|
+
if (sub === 'on') return startProfile(rest);
|
|
245
|
+
if (sub === 'off' && !rest.length) {
|
|
246
|
+
// One that ended by itself is printed once it has been read.
|
|
247
|
+
const ended = !prof && finishing && !finishing.shown;
|
|
248
|
+
if (!prof && !ended) { out.log('Not profiling.'); return; }
|
|
249
|
+
if (ended) out.log('The profile had already stopped:');
|
|
250
|
+
if (prof) prof.stoppedByOff = true;
|
|
251
|
+
await finishProfile();
|
|
252
|
+
await finishing;
|
|
253
|
+
printProfile(last, all);
|
|
254
|
+
out.log(hints([['profile save <file>', 'write the tab\'s trace, for DevTools\' Performance panel']]));
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
await finishing;
|
|
258
|
+
if (sub === 'save') return saveProfile(rest.join(' '));
|
|
259
|
+
if (sub === 'reload' && !rest.length) return profileReload(all);
|
|
260
|
+
if (sub) throw new Error(`Usage: profile on [seconds] [--cpu] | off | save <file> | reload`);
|
|
261
|
+
if (prof) {
|
|
262
|
+
out.log(`Profiling ${prof.page.url()} since ${clock(prof.startedAt)}${prof.owner ? `, for ${prof.owner}` : ''}`);
|
|
263
|
+
out.log(hints([['profile off', 'stop and print a summary']]));
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (last) {
|
|
267
|
+
out.log(`Not profiling. The last profile, from ${clock(last.startedAt)}:`);
|
|
268
|
+
printProfile(last, all);
|
|
269
|
+
} else {
|
|
270
|
+
out.log('Not profiling.');
|
|
271
|
+
}
|
|
272
|
+
out.log(hints([
|
|
273
|
+
['profile on [seconds]', 'record what the selected tab\'s main thread does'],
|
|
274
|
+
['profile reload', 'profile the selected tab\'s page as it loads'],
|
|
275
|
+
]));
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
async function profileReload(all) {
|
|
280
|
+
if (prof) throw new Error(`Already profiling ${prof.page.url()}; profile off stops it`);
|
|
281
|
+
if (!state.page || state.page.isClosed()) throw new Error(NO_TAB);
|
|
282
|
+
const page = state.page;
|
|
283
|
+
await startProfile([], true);
|
|
284
|
+
let note = null;
|
|
285
|
+
try {
|
|
286
|
+
await page.reload({ waitUntil: 'load' });
|
|
287
|
+
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => { note = 'Its network was still busy 10s after load; the profile stops there.'; });
|
|
288
|
+
// Its largest paint and the work after load land in the next frames.
|
|
289
|
+
await page.waitForTimeout(500);
|
|
290
|
+
} finally {
|
|
291
|
+
await finishProfile();
|
|
292
|
+
}
|
|
293
|
+
printProfile(last, all);
|
|
294
|
+
if (note) out.log(note);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function saveProfile(file) {
|
|
298
|
+
if (!file) throw new Error('Usage: profile save <file>, e.g. profile save load.json');
|
|
299
|
+
if (prof) throw new Error('Still profiling; profile off stops it first');
|
|
300
|
+
if (!last?.saveFile || !fs.existsSync(last.saveFile)) throw new Error('No profile to save: profile on records one first');
|
|
301
|
+
if (fs.existsSync(file)) throw new Error(`${file} exists already; name another file`);
|
|
302
|
+
if (!fs.existsSync(path.dirname(path.resolve(file)))) throw new Error(`No folder ${path.dirname(path.resolve(file))} to save ${path.basename(file)} in`);
|
|
303
|
+
fs.copyFileSync(last.saveFile, file, fs.constants.COPYFILE_EXCL);
|
|
304
|
+
const kb = Math.round(fs.statSync(file).size / 1024);
|
|
305
|
+
out.log(`Saved the profile of ${last.url} to ${file} (${kb} KB): the tab's events only. Chrome DevTools' Performance panel (Load profile) and ui.perfetto.dev open it.`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// A profile must not outlive the REPL: Chrome traces one at a time, for the whole browser.
|
|
309
|
+
onShutdown(async () => {
|
|
310
|
+
if (prof) {
|
|
311
|
+
const { session } = prof;
|
|
312
|
+
prof = null;
|
|
313
|
+
await session.send('Tracing.end').catch(() => {});
|
|
314
|
+
await session.detach().catch(() => {});
|
|
315
|
+
}
|
|
316
|
+
if (dir) fs.rmSync(dir, { recursive: true, force: true });
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
module.exports = { commands, currentProfile, finishProfile, printProfile };
|
package/lib/runner.js
CHANGED
|
@@ -33,10 +33,10 @@ const READ_ONLY = new Set(['info', 'listeners', 'text', 'html', 'attrs', 'count'
|
|
|
33
33
|
'snapshot', 'screenshot', 'wait', 'sleep', 'requests', 'body', 'console', 'cookies', 'storage', 'capture', 'help']);
|
|
34
34
|
|
|
35
35
|
// Commands that work with no tab selected.
|
|
36
|
-
const NO_TAB_NEEDED = new Set(['tab', 'modes', 'capture', 'record', 'dialog', 'help', 'quit', 'tab-select', 'close']);
|
|
36
|
+
const NO_TAB_NEEDED = new Set(['tab', 'modes', 'capture', 'profile', 'record', 'dialog', 'help', 'quit', 'tab-select', 'close']);
|
|
37
37
|
|
|
38
38
|
// Commands that accept --all, right after the command or at the end, to lift the output limit.
|
|
39
|
-
const INSPECTION = ['info', 'listeners', 'text', 'html', 'attrs', 'links', 'inputs', 'eval', 'cdp', 'cookies', 'storage', 'capture', 'body', 'console', 'snapshot', 'watch'];
|
|
39
|
+
const INSPECTION = ['info', 'listeners', 'text', 'html', 'attrs', 'links', 'inputs', 'eval', 'cdp', 'cookies', 'storage', 'capture', 'profile', 'body', 'console', 'snapshot', 'watch'];
|
|
40
40
|
|
|
41
41
|
let queue = Promise.resolve();
|
|
42
42
|
// The server command running now, so a quit at the prompt can answer it.
|
package/lib/send.js
CHANGED
|
@@ -114,7 +114,8 @@ function commandLine(words) {
|
|
|
114
114
|
return words[n - 1] === '--filename' || !/^(?:\d+|-.*)$/.test(w) ? resolve(w) : w;
|
|
115
115
|
});
|
|
116
116
|
}
|
|
117
|
-
// watch save
|
|
117
|
+
// watch save and profile save write their files.
|
|
118
|
+
if (words[0] === 'profile' && words[1] === 'save' && words.length > 2) resolved = ['profile', 'save', resolve(words.slice(2).join(' '))];
|
|
118
119
|
if (words[0] === 'watch' && words[1] === 'save' && words.length > 2) resolved = ['watch', 'save', resolve(words.slice(2).join(' '))];
|
|
119
120
|
if (words[0] === 'screenshot') {
|
|
120
121
|
resolved = words.map((w, n) => (w.startsWith('--filename=') && w.length > 11 ? `--filename=${resolve(w.slice(11))}` : words[n - 1] === '--filename' ? resolve(w) : w));
|
|
@@ -131,7 +132,7 @@ function commandLine(words) {
|
|
|
131
132
|
|
|
132
133
|
// A file of commands, one per line, run in turn until one fails. A recording it started and did not
|
|
133
134
|
// stop is still on: the next take would fail to start, so it says so.
|
|
134
|
-
const PATHS = /^(?:upload|record|video-start|video-stop|screenshot|watch)$/;
|
|
135
|
+
const PATHS = /^(?:upload|record|video-start|video-stop|screenshot|watch|profile)$/;
|
|
135
136
|
// The values of the {{ PW_NAME }} variables in a command, from send's environment: { vars }, null with
|
|
136
137
|
// none, or { error } for one that is not set or has a line break.
|
|
137
138
|
function variablesFor(command) {
|
package/lib/tabs.js
CHANGED