mirror of
https://github.com/electron/electron.git
synced 2026-05-02 03:00:22 -04:00
297 lines
11 KiB
TypeScript
297 lines
11 KiB
TypeScript
import { app, contentTracing, TraceConfig, TraceCategoriesAndOptions } from 'electron/main';
|
|
|
|
import { expect } from 'chai';
|
|
import { beforeEach, describe, it } from 'vitest';
|
|
|
|
import * as fs from 'node:fs';
|
|
import * as path from 'node:path';
|
|
import { setTimeout } from 'node:timers/promises';
|
|
|
|
import { ifdescribe } from './lib/spec-helpers';
|
|
|
|
// FIXME: The tests are skipped on linux arm/arm64
|
|
ifdescribe(!['arm', 'arm64'].includes(process.arch) || process.platform !== 'linux')('contentTracing', () => {
|
|
const record = async (
|
|
options: TraceConfig | TraceCategoriesAndOptions,
|
|
outputFilePath: string | undefined,
|
|
recordTimeInMilliseconds = 1e1
|
|
) => {
|
|
await app.whenReady();
|
|
|
|
await contentTracing.startRecording(options);
|
|
await setTimeout(recordTimeInMilliseconds);
|
|
const resultFilePath = await contentTracing.stopRecording(outputFilePath);
|
|
|
|
return resultFilePath;
|
|
};
|
|
|
|
const outputFilePath = path.join(app.getPath('temp'), 'trace.json');
|
|
beforeEach(() => {
|
|
if (fs.existsSync(outputFilePath)) {
|
|
fs.unlinkSync(outputFilePath);
|
|
}
|
|
});
|
|
|
|
// WOA needs more time
|
|
describe('startRecording', { timeout: process.platform === 'win32' && process.arch === 'arm64' ? 10e3 : 5e3 }, () => {
|
|
const getFileSizeInKiloBytes = (filePath: string) => {
|
|
const stats = fs.statSync(filePath);
|
|
const fileSizeInBytes = stats.size;
|
|
const fileSizeInKiloBytes = fileSizeInBytes / 1024;
|
|
return fileSizeInKiloBytes;
|
|
};
|
|
|
|
it('accepts an empty config', async () => {
|
|
const config = {};
|
|
await record(config, outputFilePath);
|
|
|
|
expect(fs.existsSync(outputFilePath)).to.be.true('output exists');
|
|
|
|
const fileSizeInKiloBytes = getFileSizeInKiloBytes(outputFilePath);
|
|
expect(fileSizeInKiloBytes).to.be.above(0, `the trace output file is empty, check "${outputFilePath}"`);
|
|
});
|
|
|
|
it('accepts a trace config', async () => {
|
|
// (alexeykuzmin): All categories are excluded on purpose,
|
|
// so only metadata gets into the output file.
|
|
const config = {
|
|
excluded_categories: ['*']
|
|
};
|
|
await record(config, outputFilePath);
|
|
|
|
// If the `excluded_categories` param above is not respected, categories
|
|
// like `node,node.environment` will be included in the output.
|
|
const content = fs.readFileSync(outputFilePath).toString();
|
|
expect(content.includes('"cat":"node,node.environment"')).to.be.false();
|
|
});
|
|
|
|
it('accepts "categoryFilter" and "traceOptions" as a config', async () => {
|
|
// (alexeykuzmin): All categories are excluded on purpose,
|
|
// so only metadata gets into the output file.
|
|
const config = {
|
|
categoryFilter: '__ThisIsANonexistentCategory__',
|
|
traceOptions: ''
|
|
};
|
|
await record(config, outputFilePath);
|
|
|
|
expect(fs.existsSync(outputFilePath)).to.be.true('output exists');
|
|
|
|
// If the `categoryFilter` param above is not respected
|
|
// the file size will be above 60KB.
|
|
const fileSizeInKiloBytes = getFileSizeInKiloBytes(outputFilePath);
|
|
const expectedMaximumFileSize = 60; // Depends on a platform.
|
|
|
|
expect(fileSizeInKiloBytes).to.be.above(0, `the trace output file is empty, check "${outputFilePath}"`);
|
|
expect(fileSizeInKiloBytes).to.be.below(
|
|
expectedMaximumFileSize,
|
|
`the trace output file is suspiciously large (${fileSizeInKiloBytes}KB),
|
|
check "${outputFilePath}"`
|
|
);
|
|
});
|
|
});
|
|
|
|
// WOA needs more time
|
|
ifdescribe(process.platform !== 'linux')(
|
|
'stopRecording',
|
|
{ timeout: process.platform === 'win32' && process.arch === 'arm64' ? 10e3 : 5e3 },
|
|
() => {
|
|
// FIXME(samuelmaddock): this test regularly flakes
|
|
it.skip('does not crash on empty string', async () => {
|
|
const options = {
|
|
categoryFilter: '*',
|
|
traceOptions: 'record-until-full,enable-sampling'
|
|
};
|
|
|
|
await contentTracing.startRecording(options);
|
|
const path = await contentTracing.stopRecording('');
|
|
expect(path).to.be.a('string').that.is.not.empty('result path');
|
|
expect(fs.statSync(path).isFile()).to.be.true('output exists');
|
|
});
|
|
|
|
it('calls its callback with a result file path', async () => {
|
|
const resultFilePath = await record(/* options */ {}, outputFilePath);
|
|
expect(resultFilePath).to.be.a('string').and.be.equal(outputFilePath);
|
|
});
|
|
|
|
it('creates a temporary file when an empty string is passed', async function () {
|
|
const resultFilePath = await record(/* options */ {}, /* outputFilePath */ '');
|
|
expect(resultFilePath).to.be.a('string').that.is.not.empty('result path');
|
|
});
|
|
|
|
it('creates a temporary file when no path is passed', async function () {
|
|
const resultFilePath = await record(/* options */ {}, /* outputFilePath */ undefined);
|
|
expect(resultFilePath).to.be.a('string').that.is.not.empty('result path');
|
|
});
|
|
|
|
it('rejects if no trace is happening', async () => {
|
|
await expect(contentTracing.stopRecording()).to.be.rejectedWith(
|
|
'Failed to stop tracing - no trace in progress'
|
|
);
|
|
});
|
|
}
|
|
);
|
|
|
|
describe('getTraceBufferUsage', { timeout: 10e3 }, () => {
|
|
it('does not crash and returns valid usage data', async () => {
|
|
await app.whenReady();
|
|
await contentTracing.startRecording({
|
|
categoryFilter: '*',
|
|
traceOptions: 'record-until-full'
|
|
});
|
|
|
|
// Yield to the event loop so the JS HandleScope from this tick is gone.
|
|
// When the Mojo response arrives it fires OnTraceBufferUsageAvailable
|
|
// as a plain Chromium task — if that callback lacks its own HandleScope
|
|
// the process will crash with "Cannot create a handle without a HandleScope".
|
|
const result = await contentTracing.getTraceBufferUsage();
|
|
|
|
expect(result).to.have.property('percentage').that.is.a('number');
|
|
expect(result).to.have.property('value').that.is.a('number');
|
|
|
|
await contentTracing.stopRecording();
|
|
});
|
|
|
|
it('returns zero usage when no trace is active', async () => {
|
|
await app.whenReady();
|
|
const result = await contentTracing.getTraceBufferUsage();
|
|
expect(result).to.have.property('percentage').that.is.a('number');
|
|
expect(result.percentage).to.equal(0);
|
|
});
|
|
});
|
|
|
|
describe('captured events', () => {
|
|
it('include V8 samples from the main process', { timeout: 60000 }, async () => {
|
|
await contentTracing.startRecording({
|
|
categoryFilter: 'disabled-by-default-v8.cpu_profiler',
|
|
traceOptions: 'record-until-full'
|
|
});
|
|
{
|
|
const start = Date.now();
|
|
let n = 0;
|
|
const f = () => {};
|
|
while (Date.now() - start < 200 && n < 500) {
|
|
await setTimeout(0);
|
|
f();
|
|
n++;
|
|
}
|
|
}
|
|
const path = await contentTracing.stopRecording();
|
|
const data = fs.readFileSync(path, 'utf8');
|
|
const parsed = JSON.parse(data);
|
|
expect(
|
|
parsed.traceEvents.some(
|
|
(x: any) => x.cat === 'disabled-by-default-v8.cpu_profiler' && x.name === 'ProfileChunk'
|
|
)
|
|
).to.be.true();
|
|
});
|
|
});
|
|
|
|
describe('node trace categories', () => {
|
|
it('captures performance.mark() as instant trace events', async function () {
|
|
const { performance } = require('node:perf_hooks');
|
|
|
|
await contentTracing.startRecording({
|
|
included_categories: ['node.perf.usertiming']
|
|
});
|
|
|
|
performance.mark('test-trace-mark');
|
|
|
|
const resultPath = await contentTracing.stopRecording();
|
|
const data = fs.readFileSync(resultPath, 'utf8');
|
|
const parsed = JSON.parse(data);
|
|
|
|
const markEvents = parsed.traceEvents.filter(
|
|
(x: any) => x.cat === 'node.perf.usertiming' && x.name === 'test-trace-mark'
|
|
);
|
|
expect(markEvents).to.have.lengthOf.at.least(1, 'should have node.perf.usertiming events for performance.mark()');
|
|
expect(markEvents[0].ph).to.equal('I', 'performance.mark() should emit instant (I) phase events');
|
|
});
|
|
|
|
it('captures performance.measure() as nestable async begin/end trace events', async function () {
|
|
const { performance } = require('node:perf_hooks');
|
|
|
|
await contentTracing.startRecording({
|
|
included_categories: ['node.perf.usertiming']
|
|
});
|
|
|
|
performance.mark('trace-measure-start');
|
|
await setTimeout(100);
|
|
performance.mark('trace-measure-end');
|
|
performance.measure('test-trace-measure', 'trace-measure-start', 'trace-measure-end');
|
|
|
|
const resultPath = await contentTracing.stopRecording();
|
|
const data = fs.readFileSync(resultPath, 'utf8');
|
|
const parsed = JSON.parse(data);
|
|
|
|
const measureEvents = parsed.traceEvents.filter(
|
|
(x: any) => x.cat === 'node.perf.usertiming' && x.name === 'test-trace-measure'
|
|
);
|
|
expect(measureEvents.some((x: any) => x.ph === 'b')).to.be.true('should have nestable async begin (b) event');
|
|
expect(measureEvents.some((x: any) => x.ph === 'e')).to.be.true('should have nestable async end (e) event');
|
|
});
|
|
|
|
it('captures node.fs.sync trace events for file operations', async function () {
|
|
await contentTracing.startRecording({
|
|
included_categories: ['node.fs.sync']
|
|
});
|
|
|
|
fs.readFileSync(__filename, 'utf8');
|
|
|
|
const resultPath = await contentTracing.stopRecording();
|
|
const data = fs.readFileSync(resultPath, 'utf8');
|
|
const parsed = JSON.parse(data);
|
|
|
|
const fsEvents = parsed.traceEvents.filter(
|
|
(x: any) => typeof x.cat === 'string' && x.cat.includes('node.fs.sync')
|
|
);
|
|
expect(fsEvents).to.have.lengthOf.at.least(1, 'should have node.fs.sync trace events');
|
|
});
|
|
|
|
it('captures multiple node categories simultaneously', async function () {
|
|
const vm = require('node:vm');
|
|
|
|
await contentTracing.startRecording({
|
|
included_categories: ['node.async_hooks', 'node.vm.script']
|
|
});
|
|
|
|
vm.runInNewContext('1 + 1');
|
|
await fs.promises.readFile(__filename, 'utf8');
|
|
|
|
const resultPath = await contentTracing.stopRecording();
|
|
const data = fs.readFileSync(resultPath, 'utf8');
|
|
const parsed = JSON.parse(data);
|
|
|
|
const asyncHooksEvents = parsed.traceEvents.filter(
|
|
(x: any) => typeof x.cat === 'string' && x.cat.includes('node.async_hooks')
|
|
);
|
|
const vmEvents = parsed.traceEvents.filter(
|
|
(x: any) => typeof x.cat === 'string' && x.cat.includes('node.vm.script')
|
|
);
|
|
expect(asyncHooksEvents).to.have.lengthOf.at.least(1, 'should have node.async_hooks events');
|
|
expect(vmEvents).to.have.lengthOf.at.least(1, 'should have node.vm.script events');
|
|
});
|
|
|
|
it('captures events using wildcard category pattern node.fs.*', async function () {
|
|
await contentTracing.startRecording({
|
|
included_categories: ['node.fs.*']
|
|
});
|
|
|
|
fs.readFileSync(__filename, 'utf8');
|
|
await fs.promises.readFile(__filename, 'utf8');
|
|
|
|
const resultPath = await contentTracing.stopRecording();
|
|
const data = fs.readFileSync(resultPath, 'utf8');
|
|
const parsed = JSON.parse(data);
|
|
|
|
const syncEvents = parsed.traceEvents.filter(
|
|
(x: any) => typeof x.cat === 'string' && x.cat.includes('node.fs.sync')
|
|
);
|
|
const asyncEvents = parsed.traceEvents.filter(
|
|
(x: any) => typeof x.cat === 'string' && x.cat.includes('node.fs.async')
|
|
);
|
|
expect(syncEvents).to.have.lengthOf.at.least(1, 'should have node.fs.sync events from wildcard pattern');
|
|
expect(asyncEvents).to.have.lengthOf.at.least(1, 'should have node.fs.async events from wildcard pattern');
|
|
});
|
|
});
|
|
});
|