(core) Adding UI for timing API

Summary:
Adding new buttons to control the `timing` API and a way to view the results
using virtual table features.

Test Plan: Added new

Reviewers: georgegevoian

Reviewed By: georgegevoian

Subscribers: paulfitz

Differential Revision: https://phab.getgrist.com/D4252
This commit is contained in:
Jarosław Sadziński
2024-05-21 18:27:06 +02:00
parent 60423edc17
commit a6ffa6096a
29 changed files with 858 additions and 144 deletions

View File

@@ -295,7 +295,7 @@ export interface TimingInfo {
/**
* Total time spend evaluating a formula.
*/
total: number;
sum: number;
/**
* Number of times the formula was evaluated (for all rows).
*/
@@ -320,9 +320,10 @@ export interface FormulaTimingInfo extends TimingInfo {
*/
export interface TimingStatus {
/**
* If true, timing info is being collected.
* If disabled then 'disabled', else 'active' or 'pending'. Pending means that the engine is busy
* and can't respond to confirm the status (but it used to be active before that).
*/
status: boolean;
status: 'active'|'pending'|'disabled';
/**
* Will be undefined if we can't get the timing info (e.g. if the document is locked by other call).
* Otherwise, contains the intermediate results gathered so far.

View File

@@ -107,6 +107,9 @@ export interface CommDocChatter extends CommMessageBase {
},
// This could also be a fine place to send updated info
// about other users of the document.
timing?: {
status: 'active'|'disabled';
}
};
}

View File

@@ -74,6 +74,7 @@ export interface OpenLocalDocResult {
clientId: string; // the docFD is meaningful only in the context of this session
doc: {[tableId: string]: TableDataAction};
log: MinimalActionGroup[];
isTimingOn: boolean;
recoveryMode?: boolean;
userOverride?: UserOverride;
docUsage?: FilteredDocUsageSummary;

View File

@@ -23,16 +23,17 @@ export namespace Sort {
* Object base representation for column expression.
*/
export interface ColSpecDetails {
colRef: number;
colRef: ColRef;
direction: Direction;
orderByChoice?: boolean;
emptyLast?: boolean;
naturalSort?: boolean;
}
/**
* Column expression type.
* Column expression type. Either number, an object, or virtual id string _vid\d+
*/
export type ColSpec = number | string;
export type ColRef = number | string;
/**
* Sort expression type, for example [1,-2, '3:emptyLast', '-4:orderByChoice']
*/
@@ -75,7 +76,7 @@ export namespace Sort {
tail.push("orderByChoice");
}
if (!tail.length) {
return +head;
return maybeNumber(head);
}
return head + (tail.length ? OPTION_SEPARATOR : "") + tail.join(FLAG_SEPARATOR);
}
@@ -92,21 +93,33 @@ export namespace Sort {
: parseColSpec(colSpec);
}
function maybeNumber(colRef: string): ColRef {
const num = parseInt(colRef, 10);
return isNaN(num) ? colRef : num;
}
function parseColSpec(colString: string): ColSpecDetails {
const REGEX = /^(-)?(\d+)(:([\w\d;]+))?$/;
if (!colString) {
throw new Error("Empty column expression");
}
const REGEX = /^(?<sign>-)?(?<colRef>(_vid)?(\d+))(:(?<flag>[\w\d;]+))?$/;
const match = colString.match(REGEX);
if (!match) {
throw new Error("Error parsing sort expression " + colString);
}
const [, sign, colRef, , flag] = match;
const {sign, colRef, flag} = match.groups || {};
const flags = flag?.split(";");
return {
colRef: +colRef,
return onlyDefined({
colRef: maybeNumber(colRef),
direction: sign === "-" ? DESC : ASC,
orderByChoice: flags?.includes("orderByChoice"),
emptyLast: flags?.includes("emptyLast"),
naturalSort: flags?.includes("naturalSort"),
};
});
}
function onlyDefined<T extends Record<string, any>>(obj: T): T{
return Object.fromEntries(Object.entries(obj).filter(([_, v]) => v !== undefined)) as T;
}
/**
@@ -138,17 +151,26 @@ export namespace Sort {
* Converts column expression order.
*/
export function setColDirection(colSpec: ColSpec, dir: Direction): ColSpec {
if (typeof colSpec === "number") {
if (typeof colSpec == "number") {
return Math.abs(colSpec) * dir;
} else if (colSpec.startsWith(VirtualId.PREFIX)) {
return dir === DESC ? `-${colSpec}` : colSpec;
} else if (colSpec.startsWith(`-${VirtualId.PREFIX}`)) {
return dir === ASC ? colSpec.slice(1) : colSpec;
} else {
return detailsToSpec({ ...parseColSpec(colSpec), direction: dir });
}
return detailsToSpec({ ...parseColSpec(colSpec), direction: dir });
}
/**
* Creates simple column expression.
*/
export function createColSpec(colRef: number, dir: Direction): ColSpec {
return colRef * dir;
export function createColSpec(colRef: ColRef, dir: Direction): ColSpec {
if (typeof colRef === "number") {
return colRef * dir;
} else {
return dir === ASC ? colRef : `-${colRef}`;
}
}
/**
@@ -187,7 +209,7 @@ export namespace Sort {
/**
* Swaps column id in column expression. Primary use for display columns.
*/
export function swapColRef(colSpec: ColSpec, colRef: number): ColSpec {
export function swapColRef(colSpec: ColSpec, colRef: ColRef): ColSpec {
if (typeof colSpec === "number") {
return colSpec >= 0 ? colRef : -colRef;
}
@@ -220,7 +242,7 @@ export namespace Sort {
* @param colRef Column id to remove
* @param newSpec New column sort options to put in place of the old one.
*/
export function replace(sortSpec: SortSpec, colRef: number, newSpec: ColSpec | ColSpecDetails): SortSpec {
export function replace(sortSpec: SortSpec, colRef: ColRef, newSpec: ColSpec | ColSpecDetails): SortSpec {
const index = findColIndex(sortSpec, colRef);
if (index >= 0) {
const updated = sortSpec.slice();
@@ -322,3 +344,26 @@ export namespace Sort {
});
}
}
let _virtualIdCounter = 1;
const _virtualSymbols = new Map<string, string>();
/**
* Creates a virtual id for virtual tables. Can remember some generated ids if called with a
* name (this feature used only in tests for now).
*
* The resulting id looks like _vid\d+.
*/
export function VirtualId(symbol = '') {
if (symbol) {
if (!_virtualSymbols.has(symbol)) {
const generated = `${VirtualId.PREFIX}${_virtualIdCounter++}`;
_virtualSymbols.set(symbol, generated);
return generated;
} else {
return _virtualSymbols.get(symbol)!;
}
} else {
return `${VirtualId.PREFIX}${_virtualIdCounter++}`;
}
}
VirtualId.PREFIX = '_vid';

View File

@@ -1,5 +1,6 @@
import {ActionSummary} from 'app/common/ActionSummary';
import {ApplyUAResult, ForkResult, PermissionDataWithExtraUsers, QueryFilters} from 'app/common/ActiveDocAPI';
import {ApplyUAResult, ForkResult, FormulaTimingInfo,
PermissionDataWithExtraUsers, QueryFilters, TimingStatus} from 'app/common/ActiveDocAPI';
import {AssistanceRequest, AssistanceResponse} from 'app/common/AssistancePrompts';
import {BaseAPI, IOptions} from 'app/common/BaseAPI';
import {BillingAPI, BillingAPIImpl} from 'app/common/BillingAPI';
@@ -512,14 +513,18 @@ export interface DocAPI {
getAssistance(params: AssistanceRequest): Promise<AssistanceResponse>;
/**
* Check if the document is currently in timing mode.
* Status is either
* - 'active' if timings are enabled.
* - 'pending' if timings are enabled but we can't get the data yet (as engine is blocked)
* - 'disabled' if timings are disabled.
*/
timing(): Promise<{status: boolean}>;
timing(): Promise<TimingStatus>;
/**
* Starts recording timing information for the document. Throws exception if timing is already
* in progress or you don't have permission to start timing.
*/
startTiming(): Promise<void>;
stopTiming(): Promise<void>;
stopTiming(): Promise<FormulaTimingInfo[]>;
}
// Operations that are supported by a doc worker.
@@ -1134,7 +1139,7 @@ export class DocAPIImpl extends BaseAPI implements DocAPI {
});
}
public async timing(): Promise<{status: boolean}> {
public async timing(): Promise<TimingStatus> {
return this.requestJson(`${this._url}/timing`);
}
@@ -1142,8 +1147,8 @@ export class DocAPIImpl extends BaseAPI implements DocAPI {
await this.request(`${this._url}/timing/start`, {method: 'POST'});
}
public async stopTiming(): Promise<void> {
await this.request(`${this._url}/timing/stop`, {method: 'POST'});
public async stopTiming(): Promise<FormulaTimingInfo[]> {
return await this.requestJson(`${this._url}/timing/stop`, {method: 'POST'});
}
private _getRecords(tableId: string, endpoint: 'data' | 'records', options?: GetRowsParams): Promise<any> {

View File

@@ -14,7 +14,7 @@ import clone = require('lodash/clone');
import pickBy = require('lodash/pickBy');
import slugify from 'slugify';
export const SpecialDocPage = StringUnion('code', 'acl', 'data', 'GristDocTour', 'settings', 'webhook');
export const SpecialDocPage = StringUnion('code', 'acl', 'data', 'GristDocTour', 'settings', 'webhook', 'timing');
type SpecialDocPage = typeof SpecialDocPage.type;
export type IDocPage = number | SpecialDocPage;