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
45 changes: 42 additions & 3 deletions log-viewer/src/features/database/components/DatabaseRowBudget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { inspectorSectionStyles } from '../../../styles/inspectorSection.styles.
import { NO_STATEMENTS } from '../services/databaseOverview.js';
import {
rowBudgets,
type ObjectRows,
type RowBudget,
type RowBudgetKind,
type RowCount,
Expand All @@ -28,9 +29,11 @@ import {
import { kindColors } from './DatabaseOverview.js';
import { governorTier } from './GovernorSummary.js';

/** More than a namespace bar: single-hue shades in a wide card, not a dock legend. */
/** More than a namespace bar: shades in a wide card, not a dock legend. */
const MAX_OBJECTS = 8;

const OBJECTS_LABEL = 'Query and DML rows by SObject';

const KIND_LABEL: Record<RowBudgetKind, string> = { SOQL: 'Query rows', DML: 'DML rows' };

/**
Expand Down Expand Up @@ -63,12 +66,16 @@ export class DatabaseRowBudget extends LitElement {
padding-top: var(--lana-space-sm);
}

.budget__head,
.objects__head {
padding-bottom: var(--lana-space-2xs);
font-size: var(--lana-text-sm);
}

.budget__head {
display: flex;
align-items: baseline;
gap: var(--lana-space-sm);
padding-bottom: var(--lana-space-2xs);
font-size: var(--lana-text-sm);
}

.budget__figure {
Expand Down Expand Up @@ -114,6 +121,7 @@ export class DatabaseRowBudget extends LitElement {
const shown = budgets.budgets.filter((budget) => (budget.used ?? budget.observed) > 0);
return html`
${shown.map((budget) => this._budget(budget, colors[budget.kind]))}
${budgets.objects.length > 0 ? this._objects(budgets.objects, colors) : ''}
${
shown.some(overLimit)
? html`<p class="note">
Expand All @@ -135,6 +143,37 @@ export class DatabaseRowBudget extends LitElement {
`;
}

/**
* Every SObject once, read beside written. The two limits are separate bars, so
* an object on both sides is only whole here, and its hue says which way it leans.
*/
private _objects(
objects: readonly ObjectRows[],
colors: Record<RowBudgetKind, string>,
): TemplateResult {
const segments = segmentsWithTail(
objects,
(object, index) => ({
label: object.sObject,
value: object.rows,
color: shade(colors[object.rowsWritten > object.rowsRead ? 'DML' : 'SOQL'], index),
detail: `${formatInteger(object.rowsRead)} read Β· ${formatInteger(object.rowsWritten)} written`,
}),
MAX_OBJECTS,
);
return html`
<div class="budget">
<p class="objects__head">${OBJECTS_LABEL}</p>
<stacked-time-bar
.format=${formatInteger}
label=${OBJECTS_LABEL}
legend
.segments=${segments}
></stacked-time-bar>
</div>
`;
}

/** One statement count, against its limit where the log names one. */
private _count(count: RowCount): TemplateResult {
const limit = count.limit > 0 ? `/${formatInteger(count.limit)}` : '';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ const full = (): RowBudgets => ({
groups: [{ sObject: 'Case', rows: 300, statements: 1 }],
},
],
objects: [
{ sObject: 'Contact', rowsRead: 40_000, rowsWritten: 0, rows: 40_000 },
{ sObject: 'Account', rowsRead: 5_000, rowsWritten: 0, rows: 5_000 },
{ sObject: 'Case', rowsRead: 0, rowsWritten: 300, rows: 300 },
],
counts: [
{ label: 'SOQL', used: 62, limit: 100 },
{ label: 'DML', used: 14, limit: 150 },
Expand Down Expand Up @@ -79,6 +84,9 @@ const texts = (element: Element, selector: string) =>
const bars = (element: Element) =>
[...(element.shadowRoot?.querySelectorAll('stacked-time-bar') ?? [])] as StackedTimeBar[];

const barOf = (element: Element, label: string) =>
bars(element).find((bar) => bar.getAttribute('label') === label);

beforeEach(() => {
document.body.replaceChildren();
budgets = full();
Expand All @@ -97,7 +105,12 @@ describe('database-rows', () => {
it('measures the bar against the limit, not against the rows it holds', async () => {
const element = await mount();

expect(bars(element).map((bar) => [bar.format(1_000), bar.total])).toEqual([
expect(
['Query rows', 'DML rows'].map((label) => {
const bar = barOf(element, label);
return [bar?.format(1_000), bar?.total];
}),
).toEqual([
['1,000', 50_000],
['1,000', 10_000],
]);
Expand Down Expand Up @@ -186,6 +199,26 @@ describe('database-rows', () => {
]);
});

it('brings the two limits together per SObject, read beside written', async () => {
const objects = barOf(await mount(), 'Query and DML rows by SObject');

expect(
objects?.segments.map((segment) => [segment.label, segment.value, segment.detail]),
).toEqual([
['Contact', 40_000, '40,000 read Β· 0 written'],
['Account', 5_000, '5,000 read Β· 0 written'],
['Case', 300, '0 read Β· 300 written'],
]);
// Rows against no one limit, so the bar measures itself.
expect(objects?.total).toBe(0);
});

it('leaves the SObject split out when the service brings nothing together', async () => {
budgets = { ...full(), objects: [] };

expect(texts(await mount(), '.objects__head')).toEqual([]);
});

it('says so when the log records no database statements', async () => {
budgets = { ...full(), statements: 0 };

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,4 +185,61 @@ describe('rowBudgets', () => {
{ sObject: UNKNOWN_OBJECT, rows: 10, statements: 1 },
]);
});

it('brings an SObject read and written together, biggest total first', () => {
overview = overviewOf([
statement({ rows: 100, maxRows: 100 }),
statement({ rows: 40, kind: 'DML', sObject: 'Account', repeats: 3 }),
statement({ rows: 60, maxRows: 60, sObject: 'Contact' }),
]);

expect(rowBudgets(logWith(1)).objects).toEqual([
{ sObject: 'Account', rowsRead: 100, rowsWritten: 40, rows: 140 },
{ sObject: 'Contact', rowsRead: 60, rowsWritten: 0, rows: 60 },
]);
});

it('leaves the split out while one limit holds every row, which is its own bar', () => {
overview = overviewOf([statement({ rows: 100, maxRows: 100 })]);

expect(rowBudgets(logWith(1)).objects).toEqual([]);
});

it('brings one SObject together when the two sides name it in a different case', () => {
overview = overviewOf([
statement({ rows: 100, maxRows: 100, sObject: 'account' }),
statement({ rows: 5, kind: 'DML', sObject: 'Account' }),
]);

expect(rowBudgets(logWith(1)).objects).toEqual([
{ sObject: 'account', rowsRead: 100, rowsWritten: 5, rows: 105 },
]);
});

it('leaves the unknown label out of the split, a bucket of objects and not one', () => {
overview = overviewOf([
statement({ rows: 100, sObject: null }),
statement({ rows: 5, kind: 'DML', sObject: null }),
statement({ rows: 10, maxRows: 10 }),
statement({ rows: 2, kind: 'DML', sObject: 'Case' }),
]);

expect(rowBudgets(logWith(1)).objects).toEqual([
{ sObject: 'Account', rowsRead: 10, rowsWritten: 0, rows: 10 },
{ sObject: 'Case', rowsRead: 0, rowsWritten: 2, rows: 2 },
]);
});

it('leaves a search out of the SObject split, which holds rows against no total', () => {
overview = overviewOf([
statement({ rows: 30, maxRows: 30, kind: 'SOSL', sObject: 'Lead' }),
statement({ rows: 10, maxRows: 10 }),
statement({ rows: 5, kind: 'DML', sObject: 'Case' }),
]);

expect(rowBudgets(logWith(1)).objects).toEqual([
{ sObject: 'Account', rowsRead: 10, rowsWritten: 0, rows: 10 },
{ sObject: 'Case', rowsRead: 0, rowsWritten: 5, rows: 5 },
]);
});
});
43 changes: 43 additions & 0 deletions log-viewer/src/features/database/services/rowBudget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ export interface RowBudget {
groups: RowGroup[];
}

/** One SObject's rows, read beside written, across every statement that touched it. */
export interface ObjectRows {
sObject: string;
rowsRead: number;
rowsWritten: number;
/** The two sides summed: the length the segment is drawn at. */
rows: number;
}

/** A statement count against its own limit, for the counts line. */
export interface RowCount {
label: string;
Expand All @@ -46,6 +55,8 @@ export interface RowCount {
/** Everything the Row budget section reads. */
export interface RowBudgets {
budgets: RowBudget[];
/** Every SObject once, read beside written, biggest first. Empty unless both limits hold rows. */
objects: ObjectRows[];
counts: RowCount[];
/** The most rows one SOSL query returned, against its per-query cap. */
worstSearch: { rows: number; limit: number } | null;
Expand Down Expand Up @@ -95,6 +106,8 @@ function build(log: ApexLog): RowBudgets {
budgetOf('SOQL', soql, limits.queryRows, hasLimits ? limits.queryRows.used : null),
budgetOf('DML', dml, limits.dmlRows, hasLimits ? limits.dmlRows.used : null),
],
// With one limit holding every row the roll-up says nothing the budget does not.
objects: soql.size > 0 && dml.size > 0 ? objectRows(soql, dml) : [],
counts: [
count('SOQL', limits.soqlQueries, overview.time.soql.statements, hasLimits),
count('DML', limits.dmlStatements, overview.time.dml.statements, hasLimits),
Expand All @@ -118,6 +131,36 @@ function addRows(groups: Map<string, RowGroup>, statement: DatabaseStatement): v
}
}

/**
* The two budgets rolled up per SObject. An object both read and written appears
* in each, and only here do its two halves meet. The two sides name the object
* from separate sources, so they can disagree on case; the unknown label is a
* bucket of objects, not one object, so it holds no roll-up.
*/
function objectRows(soql: Map<string, RowGroup>, dml: Map<string, RowGroup>): ObjectRows[] {
const merged = new Map<string, ObjectRows>();
const addSide = (groups: Map<string, RowGroup>, side: 'rowsRead' | 'rowsWritten'): void => {
for (const group of groups.values()) {
if (group.sObject === UNKNOWN_OBJECT) {
continue;
}
const key = group.sObject.toLowerCase();
const object = merged.get(key) ?? {
sObject: group.sObject,
rowsRead: 0,
rowsWritten: 0,
rows: 0,
};
object[side] += group.rows;
object.rows += group.rows;
merged.set(key, object);
}
};
addSide(soql, 'rowsRead');
addSide(dml, 'rowsWritten');
return [...merged.values()].sort((a, b) => b.rows - a.rows);
}

/** The governor's count where the log holds one, else what the tree showed. */
function count(
label: string,
Expand Down