openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
212 lines • 9.42 kB
JavaScript
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import zlib from 'node:zlib';
import { zod, DevTools } from '../third_party/index.js';
import { parseRawTraceBuffer, traceResultIsSuccess, } from '../processors/PerformanceTrace.js';
import { logger } from '../utils/logger.js';
import { ToolCategory } from './categories.js';
import { definePageTool } from './ToolDefinition.js';
const filePathSchema = zod
.string()
.optional()
.describe('The absolute file path, or a file path relative to the current working directory, to save the raw trace data. For example, trace.json.gz (compressed) or trace.json (uncompressed).');
export const startTrace = definePageTool(() => ({
name: 'performance_start_trace',
description: `Start a performance trace on the target webpage. Use to find frontend performance issues, Core Web Vitals (LCP, INP, CLS), and improve page load speed.`,
annotations: {
category: ToolCategory.PERFORMANCE,
readOnlyHint: false,
},
schema: {
reload: zod
.boolean()
.default(true)
.describe('Determines if, once tracing has started, the target page should be automatically reloaded. Navigate the page to the right URL using the navigate_page tool BEFORE starting the trace if reload or autoStop is set to true.'),
autoStop: zod
.boolean()
.default(true)
.describe('Determines if the trace recording should be automatically stopped.'),
filePath: filePathSchema,
},
blockedByDialog: true,
verifyFilesSchema: {
filePath: true,
},
handler: async (request, response, context) => {
if (context.isRunningPerformanceTrace()) {
response.appendResponseLine('Error: a performance trace is already running. Use performance_stop_trace to stop it. Only one trace can be running at any given time.');
return;
}
context.setIsRunningPerformanceTrace(true);
const page = request.page;
const pageUrlForTracing = page.pptrPage.url();
try {
if (request.params.reload) {
// Before starting the recording, navigate to about:blank to clear out any state.
// We use `load` because `networkidle0` is known to be flaky for about:blank in Puppeteer.
await page.pptrPage.goto('about:blank', {
waitUntil: 'load',
});
}
const categories = [
'-*',
...DevTools.TracingDefaultCategories,
// These categories are optional in DevTools, but enabled by default in the DevTools UI, so we enable them here too.
...DevTools.TracingOptionalCategories.JsSampling,
...DevTools.TracingOptionalCategories.Screenshot,
];
await page.pptrPage.tracing.start({
categories,
bufferSize: 1200 * 1000,
});
if (request.params.reload) {
await page.pptrPage.goto(pageUrlForTracing, {
waitUntil: ['load'],
});
}
if (request.params.autoStop) {
await new Promise(resolve => setTimeout(resolve, 5_000));
await stopTracingAndAppendOutput(page, response, context, request.params.filePath);
}
else {
response.appendResponseLine(`The performance trace is being recorded. Use performance_stop_trace to stop it.`);
}
}
catch (error) {
// If a setup step (navigation, tracing.start) throws before
// stopTracingAndAppendOutput runs, the running flag would otherwise stay
// stuck `true` for the rest of the session, blocking all future traces.
// Unwind here: stop tracing if it was started and clear the flag. When
// autoStop already ran stopTracingAndAppendOutput the flag is already
// false and this is a no-op.
if (context.isRunningPerformanceTrace()) {
try {
await page.pptrPage.tracing.stop();
}
catch {
// Tracing may not have started yet; ignore.
}
context.setIsRunningPerformanceTrace(false);
}
throw error;
}
},
}));
export const stopTrace = definePageTool(() => ({
name: 'performance_stop_trace',
description: 'Stop the active performance trace recording on the target webpage.',
annotations: {
category: ToolCategory.PERFORMANCE,
readOnlyHint: false,
},
schema: {
filePath: filePathSchema,
},
blockedByDialog: true,
verifyFilesSchema: {
filePath: true,
},
handler: async (request, response, context) => {
if (!context.isRunningPerformanceTrace()) {
return;
}
const page = request.page;
await stopTracingAndAppendOutput(page, response, context, request.params.filePath);
},
}));
export const analyzeInsight = definePageTool(() => ({
name: 'performance_analyze_insight',
description: 'Provides more detailed information on a specific Performance Insight of an insight set that was highlighted in the results of a trace recording.',
annotations: {
category: ToolCategory.PERFORMANCE,
readOnlyHint: true,
},
schema: {
insightSetId: zod
.string()
.describe('The id for the specific insight set. Only use the ids given in the "Available insight sets" list.'),
insightName: zod
.string()
.describe('The name of the Insight you want more information on. For example: "DocumentLatency" or "LCPBreakdown"'),
},
blockedByDialog: false,
verifyFilesSchema: {},
handler: async (request, response, context) => {
const lastRecording = context.recordedTraces().at(-1);
if (!lastRecording) {
response.appendResponseLine('No recorded traces found. Record a performance trace so you have Insights to analyze.');
return;
}
response.attachTraceInsight(lastRecording, request.params.insightSetId, request.params.insightName);
},
}));
async function stopTracingAndAppendOutput(page, response, context, filePath) {
try {
const traceEventsBuffer = await page.pptrPage.tracing.stop();
if (filePath && traceEventsBuffer) {
let dataToWrite = traceEventsBuffer;
if (filePath.endsWith('.gz')) {
dataToWrite = await new Promise((resolve, reject) => {
zlib.gzip(traceEventsBuffer, (error, result) => {
if (error) {
reject(error);
}
else {
resolve(result);
}
});
});
}
const file = await context.saveFile(dataToWrite, filePath, filePath.endsWith('.gz') ? '.gz' : '.json');
response.appendResponseLine(`The raw trace data was saved to ${file.filename}.`);
}
const result = await parseRawTraceBuffer(traceEventsBuffer, {
cpuThrottling: page.cpuThrottlingRate,
networkThrottling: page.networkConditions ?? undefined,
});
response.appendResponseLine('The performance trace has been stopped.');
if (traceResultIsSuccess(result)) {
if (context.isCruxEnabled()) {
await populateCruxData(result);
}
context.storeTraceRecording(result);
response.attachTraceSummary(result);
}
else {
throw new Error(`There was an unexpected error parsing the trace: ${result.error}`);
}
}
finally {
context.setIsRunningPerformanceTrace(false);
}
}
/** We tell CrUXManager to fetch data so it's available when DevTools.PerformanceTraceFormatter is invoked */
async function populateCruxData(result) {
logger?.('populateCruxData called');
const cruxManager = DevTools.CrUXManager.CrUXManager.instance();
// go/jtfbx. Yes, we're aware this API key is public. ;)
cruxManager.setEndpointForTesting('https://chromeuxreport.googleapis.com/v1/records:queryRecord?key=AIzaSyBn5gimNjhiEyA_euicSKko6IlD3HdgUfk');
const cruxSetting = DevTools.Common.Settings.Settings.instance().createSetting('field-data', {
enabled: true,
});
cruxSetting.set({ enabled: true });
// Gather URLs to fetch CrUX data for
const urls = [...(result.parsedTrace.insights?.values() ?? [])].map(c => c.url.toString());
urls.push(result.parsedTrace.data.Meta.mainFrameURL);
const urlSet = new Set(urls);
if (urlSet.size === 0) {
logger?.('No URLs found for CrUX data');
return;
}
logger?.(`Fetching CrUX data for ${urlSet.size} URLs: ${Array.from(urlSet).join(', ')}`);
const cruxData = await Promise.all(Array.from(urlSet).map(async (url) => {
const data = await cruxManager.getFieldDataForPage(url);
logger?.(`CrUX data for ${url}: ${data ? 'found' : 'not found'}`);
return data;
}));
result.parsedTrace.metadata.cruxFieldData = cruxData;
}
//# sourceMappingURL=performance.js.map