(core) New API to collect timing information from formula evaluation.

Summary:
- /timing/start endpoint to start collecting information
- /timing/stop endpoint to stop collecting
- /timing to retrive data gatherd so far

Timings are collected for all columns (including hidden/helpers/system)

Test Plan: Added new

Reviewers: paulfitz

Reviewed By: paulfitz

Differential Revision: https://phab.getgrist.com/D4230
This commit is contained in:
Jarosław Sadziński
2024-04-18 14:13:16 +02:00
parent c187ca3093
commit bd07e9c026
12 changed files with 530 additions and 46 deletions

View File

@@ -22,6 +22,7 @@ import {
ApplyUAResult,
DataSourceTransformed,
ForkResult,
FormulaTimingInfo,
ImportOptions,
ImportResult,
ISuggestionWithValue,
@@ -220,6 +221,7 @@ export class ActiveDoc extends EventEmitter {
public docData: DocData|null = null;
// Used by DocApi to only allow one webhook-related endpoint to run at a time.
public readonly triggersLock: Mutex = new Mutex();
public isTimingOn = false;
protected _actionHistory: ActionHistory;
protected _docManager: DocManager;
@@ -1366,6 +1368,7 @@ export class ActiveDoc extends EventEmitter {
*/
public async reloadDoc(docSession?: DocSession) {
this._log.debug(docSession || null, 'ActiveDoc.reloadDoc starting shutdown');
this._docManager.restoreTimingOn(this.docName, this.isTimingOn);
return this.shutdown();
}
@@ -1870,6 +1873,40 @@ export class ActiveDoc extends EventEmitter {
return await this._getHomeDbManagerOrFail().getShareByLinkId(this.docName, linkId);
}
public async startTiming(): Promise<void> {
// Set the flag to indicate that timing is on.
this.isTimingOn = true;
try {
// Call the data engine to start timing.
await this._doStartTiming();
} catch (e) {
this.isTimingOn = false;
throw e;
}
// Mark self as in timing mode, in case we get reloaded.
this._docManager.restoreTimingOn(this.docName, true);
}
public async stopTiming(): Promise<FormulaTimingInfo[]> {
// First call the data engine to stop timing, and gather results.
const timingResults = await this._pyCall('stop_timing');
// Toggle the flag and clear the reminder.
this.isTimingOn = false;
this._docManager.restoreTimingOn(this.docName, false);
return timingResults;
}
public async getTimings(): Promise<FormulaTimingInfo[]|void> {
if (this._modificationLock.isLocked()) {
return;
}
return await this._pyCall('get_timings');
}
/**
* Loads an open document from DocStorage. Returns a list of the tables it contains.
*/
@@ -2377,6 +2414,10 @@ export class ActiveDoc extends EventEmitter {
});
await this._pyCall('initialize', this._options?.docUrl);
if (this.isTimingOn) {
await this._doStartTiming();
}
// Calculations are not associated specifically with the user opening the document.
// TODO: be careful with which users can create formulas.
await this._applyUserActions(makeExceptionalDocSession('system'), [['Calculate']]);
@@ -2686,7 +2727,9 @@ export class ActiveDoc extends EventEmitter {
}
private async _getEngine(): Promise<ISandbox> {
if (this._shuttingDown) { throw new Error('shutting down, data engine unavailable'); }
if (this._shuttingDown) {
throw new Error('shutting down, data engine unavailable');
}
if (this._dataEngine) { return this._dataEngine; }
this._dataEngine = this._isSnapshot ? this._makeNullEngine() : this._makeEngine();
@@ -2830,6 +2873,10 @@ export class ActiveDoc extends EventEmitter {
return dbManager;
}
private _doStartTiming() {
return this._pyCall('start_timing');
}
}
// Helper to initialize a sandbox action bundle with no values.

View File

@@ -1556,6 +1556,40 @@ export class DocWorkerApi {
});
})
);
// GET /api/docs/:docId/timings
// Checks if timing is on for the document.
this._app.get('/api/docs/:docId/timing', isOwner, withDoc(async (activeDoc, req, res) => {
if (!activeDoc.isTimingOn) {
res.json({status: 'disabled'});
} else {
const timing = await activeDoc.getTimings();
const status = timing ? 'active' : 'pending';
res.json({status, timing});
}
}));
// POST /api/docs/:docId/timings/start
// Start a timing for the document.
this._app.post('/api/docs/:docId/timing/start', isOwner, withDoc(async (activeDoc, req, res) => {
if (activeDoc.isTimingOn) {
res.status(400).json({error:`Timing already started for ${activeDoc.docName}`});
return;
}
// isTimingOn flag is switched synchronously.
await activeDoc.startTiming();
res.sendStatus(200);
}));
// POST /api/docs/:docId/timings/stop
// Stop a timing for the document.
this._app.post('/api/docs/:docId/timing/stop', isOwner, withDoc(async (activeDoc, req, res) => {
if (!activeDoc.isTimingOn) {
res.status(400).json({error:`Timing not started for ${activeDoc.docName}`});
return;
}
res.json(await activeDoc.stopTiming());
}));
}
/**

View File

@@ -43,7 +43,10 @@ export const DEFAULT_CACHE_TTL = 10000;
// How long to remember that a document has been explicitly set in a
// recovery mode.
export const RECOVERY_CACHE_TTL = 30000;
export const RECOVERY_CACHE_TTL = 30000; // 30 seconds
// How long to remember the timing mode of a document.
export const TIMING_ON_CACHE_TTL = 30000; // 30 seconds
/**
* DocManager keeps track of "active" Grist documents, i.e. those loaded
@@ -56,6 +59,9 @@ export class DocManager extends EventEmitter {
// Remember recovery mode of documents.
private _inRecovery = new MapWithTTL<string, boolean>(RECOVERY_CACHE_TTL);
// Remember timing mode of documents, when document is recreated it is put in the same mode.
private _inTimingOn = new MapWithTTL<string, boolean>(TIMING_ON_CACHE_TTL);
constructor(
public readonly storageManager: IDocStorageManager,
public readonly pluginManager: PluginManager|null,
@@ -69,6 +75,13 @@ export class DocManager extends EventEmitter {
this._inRecovery.set(docId, recovery);
}
/**
* Will restore timing on a document when it is reloaded.
*/
public restoreTimingOn(docId: string, timingOn: boolean) {
this._inTimingOn.set(docId, timingOn);
}
// attach a home database to the DocManager. During some tests, it
// is awkward to have this set up at the point of construction.
public testSetHomeDbManager(dbManager: HomeDBManager) {
@@ -437,6 +450,10 @@ export class DocManager extends EventEmitter {
log.error('DocManager had problem shutting down storage: %s', err.message);
}
// Clear any timeouts we might have.
this._inRecovery.clear();
this._inTimingOn.clear();
// Clear the setInterval that the pidusage module sets up internally.
pidusage.clear();
}
@@ -601,7 +618,10 @@ export class DocManager extends EventEmitter {
const doc = await this._getDoc(docSession, docName);
// Get URL for document for use with SELF_HYPERLINK().
const docUrls = doc && await this._getDocUrls(doc);
return new ActiveDoc(this, docName, {...docUrls, safeMode, doc});
const activeDoc = new ActiveDoc(this, docName, {...docUrls, safeMode, doc});
// Restore the timing mode of the document.
activeDoc.isTimingOn = this._inTimingOn.get(docName) || false;
return activeDoc;
}
/**

View File

@@ -132,6 +132,8 @@ export class DocWorker {
getUsersForViewAs: activeDocMethod.bind(null, 'viewers', 'getUsersForViewAs'),
getAccessToken: activeDocMethod.bind(null, 'viewers', 'getAccessToken'),
getShare: activeDocMethod.bind(null, 'owners', 'getShare'),
startTiming: activeDocMethod.bind(null, 'owners', 'startTiming'),
stopTiming: activeDocMethod.bind(null, 'owners', 'stopTiming'),
});
}