Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions src/__tests__/Details.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,61 @@ describe('FLOW_ELEMENT_ERROR details', () => {
});
});

describe('FLOW_START_INTERVIEWS_BEGIN details', () => {
const interviews = (unit: string | null): string => {
const body = [
'09:18:22.6 (300)|FLOW_START_INTERVIEWS_BEGIN|1',
'09:18:22.6 (310)|FLOW_START_INTERVIEWS_END|1',
];
return unit === null
? body.join('\n')
: [
`09:18:22.6 (200)|CODE_UNIT_STARTED|[EXTERNAL]|${unit}`,
...body,
`09:18:22.6 (400)|CODE_UNIT_FINISHED|${unit}`,
].join('\n');
};
const flowTypes = (text: string) =>
parse(text)
.ofType('FLOW_START_INTERVIEWS_BEGIN')
.map((e) => e.details.flowType);

it.each([
['a Flow code unit', 'Flow:01I000000000AAA', 'flow'],
['a Workflow code unit', 'Workflow:01I000000000AAA', 'processBuilder'],
['a code unit of another kind', 'Validation:MyObject:a00000000000AAA', null],
['no code unit', null, null],
])('is %s: %s', (_name, unit, expected) => {
expect(flowTypes(interviews(unit))).toEqual([expected]);
});

it('is null when the nearest code unit is of another kind, though a Flow is above it', () => {
const text = [
'09:18:22.6 (200)|CODE_UNIT_STARTED|[EXTERNAL]|Flow:01I000000000AAA',
'09:18:22.6 (210)|CODE_UNIT_STARTED|[EXTERNAL]|Validation:MyObject:a00000000000AAA',
'09:18:22.6 (300)|FLOW_START_INTERVIEWS_BEGIN|1',
'09:18:22.6 (310)|FLOW_START_INTERVIEWS_END|1',
'09:18:22.6 (390)|CODE_UNIT_FINISHED|Validation:MyObject:a00000000000AAA',
'09:18:22.6 (400)|CODE_UNIT_FINISHED|Flow:01I000000000AAA',
].join('\n');
expect(flowTypes(text)).toEqual([null]);
});

it('is flow for interviews inside another interviews frame, whatever started it', () => {
const text = [
'09:18:22.6 (200)|CODE_UNIT_STARTED|[EXTERNAL]|Workflow:01I000000000AAA',
'09:18:22.6 (300)|FLOW_START_INTERVIEWS_BEGIN|1',
'09:18:22.6 (310)|FLOW_START_INTERVIEW_BEGIN|01I000000000AAA|Outer',
'09:18:22.6 (320)|FLOW_START_INTERVIEWS_BEGIN|1',
'09:18:22.6 (330)|FLOW_START_INTERVIEWS_END|1',
'09:18:22.6 (340)|FLOW_START_INTERVIEW_END|01I000000000AAA|Outer',
'09:18:22.6 (350)|FLOW_START_INTERVIEWS_END|1',
'09:18:22.6 (400)|CODE_UNIT_FINISHED|Workflow:01I000000000AAA',
].join('\n');
expect(flowTypes(text)).toEqual(['processBuilder', 'flow']);
});
});

describe('limit and heap details', () => {
const log = parse(
inExecution(
Expand Down
32 changes: 28 additions & 4 deletions src/views/details.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@ export interface ExplainPlan {
readonly relativeCost: number | null;
}

/** What started a flow interviews frame. */
export type FlowType = 'flow' | 'processBuilder';

const FLOW_OF_UNIT = new Map<string, FlowType>([
['Flow', 'flow'],
['Workflow', 'processBuilder'],
]);

/** The flow type a code unit of `unit` type starts; null for any other unit, or none. */
export const flowTypeOfUnit = (unit: string | null): FlowType | null =>
FLOW_OF_UNIT.get(unit ?? '') ?? null;

/**
* The values each type's line states beyond its text, or derives from the rows after it, by type. A
* value is null when the log does not state it, or states it malformed; `field()` still gives the
Expand All @@ -45,6 +57,8 @@ export interface EventDetails {
readonly DML_BEGIN: { readonly operation: string | null; readonly sObjectType: string | null };
/** The kind of code, as `Flow` or `Validation`, from the unit's name. */
readonly CODE_UNIT_STARTED: { readonly codeUnitType: string | null };
/** What started the interviews, from the code unit above them: `Flow` or nested is `flow`, `Workflow` is `processBuilder`. Null for any other unit. */
readonly FLOW_START_INTERVIEWS_BEGIN: { readonly flowType: FlowType | null };
/** Bytes, as the line states them: positive for a deallocation. */
readonly HEAP_ALLOCATE: { readonly bytes: number | null };
readonly HEAP_DEALLOCATE: { readonly bytes: number | null };
Expand Down Expand Up @@ -80,10 +94,16 @@ export type AnyDetails = EventDetails[keyof EventDetails];

type Read<T> = (f: Fields) => T;
/**
* Reads a line's details; `explain` gives a query's explain line, or null when it has none, and
* `isCaught` the reading of a throw.
* Reads a line's details; `explain` gives a query's explain line, or null when it has none,
* `isCaught` the reading of a throw, and `flowType` what started an interviews frame. `explain`
* and `flowType` move the shared cursor, so read `f` first.
*/
type Reader<T> = (f: Fields, explain: () => Fields | null, isCaught: () => boolean | null) => T;
type Reader<T> = (
f: Fields,
explain: () => Fields | null,
isCaught: () => boolean | null,
flowType: () => FlowType | null,
) => T;

/** Field `name`'s position in `typeId`'s line; throws at load for a name the entry does not list. */
function position(typeId: number, name: string): { at: number; last: boolean } {
Expand Down Expand Up @@ -175,6 +195,9 @@ reader('DML_BEGIN', (t) => {
return (f) => ({ operation: operation(f), sObjectType: sObjectType(f) });
});
reader('CODE_UNIT_STARTED', () => (f) => ({ codeUnitType: codeUnitType(f) }));
reader('FLOW_START_INTERVIEWS_BEGIN', () => (_f, _explain, _isCaught, flowType) => ({
flowType: flowType(),
}));
for (const type of ['HEAP_ALLOCATE', 'HEAP_DEALLOCATE', 'BULK_HEAP_ALLOCATE'] as const) {
reader(type, (t) => {
const bytes = int(t, 'bytes', 'Bytes:');
Expand Down Expand Up @@ -216,8 +239,9 @@ export function eventDetails(
f: () => Fields,
explain: () => Fields | null,
isCaught: () => boolean | null,
flowType: () => FlowType | null,
): AnyDetails | null {
const read = READERS[typeId];
const details = read ? read(f(), explain, isCaught) : null;
const details = read ? read(f(), explain, isCaught, flowType) : null;
return details && Object.freeze(details);
}
26 changes: 23 additions & 3 deletions src/views/lines.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,22 @@
*/
import { ByteFields } from '../bytes/cursor.js';
import type { Source } from '../bytes/source.js';
import { EVENT_TYPES, eventText, fieldPosition, idOfType } from '../catalog/catalog.js';
import {
codeUnitType,
EVENT_TYPES,
eventText,
fieldPosition,
idOfType,
} from '../catalog/catalog.js';
import type { Issues } from '../engine/issues.js';
import { throwOutcomes } from '../engine/throws.js';
import type { Store } from '../store/store.js';
import type { AnyDetails } from './details.js';
import { eventDetails } from './details.js';
import type { AnyDetails, FlowType } from './details.js';
import { eventDetails, flowTypeOfUnit } from './details.js';

const INTERVIEWS = idOfType('FLOW_START_INTERVIEWS_BEGIN');
const INTERVIEW = idOfType('FLOW_START_INTERVIEW_BEGIN');
const CODE_UNIT_STARTED = idOfType('CODE_UNIT_STARTED');
const EXPLAIN = idOfType('SOQL_EXECUTE_EXPLAIN');

/**
Expand Down Expand Up @@ -71,6 +78,7 @@ export class EventLines {
return plan < 0 ? null : this.at(plan);
},
() => (this.outcomes ??= throwOutcomes(this.store, this.issues)).get(id) ?? null,
() => this.flowTypeOf(id),
);
}

Expand All @@ -86,6 +94,18 @@ export class EventLines {
return this.fields;
}

/** What started the interviews: the nearest code unit or interviews frame above them. */
private flowTypeOf(id: number): FlowType | null {
const { parent } = this.store;
// Every parent is a row; row 0, the log, ends the walk.
for (let up = parent[id]!; up > 0; up = parent[up]!) {
const type = this.typeOf(up);
if (type === INTERVIEWS) return 'flow';
if (type === CODE_UNIT_STARTED) return flowTypeOfUnit(codeUnitType(this.at(up)));
}
return null;
}

private firstChildOf(id: number, type: number): number {
const { subtreeEnd } = this.store;
// Each child's subtree ends where its next sibling starts; every id here is a row.
Expand Down
Loading