Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
62 changes: 58 additions & 4 deletions .github/workflows/ingest-agentic-results.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ on:
required: false
default: production
type: string
neon-branch:
description: Named Neon branch when database-target is neon-branch
required: false
default: ''
type: string
preview-url:
description: Preview deployment URL to invalidate for a named Neon branch
required: false
default: ''
type: string
secrets:
DATABASE_WRITE_URL:
description: Production database write connection for direct ingests
Expand All @@ -60,6 +70,12 @@ on:
INFX_MAIN_PAT:
description: Token used to download InferenceX artifacts
required: true
NEON_API_KEY:
description: Neon API key used to resolve named child branches
required: false
NEON_PROJECT_ID:
description: Neon project containing named child branches
required: false
SLACK_WEBHOOK_URL:
description: Optional failure and unmapped-entity notifications
required: false
Expand All @@ -83,6 +99,17 @@ on:
- production
- dev
- staging
- neon-branch
neon-branch:
description: Named Neon branch when database-target is neon-branch
required: false
default: ''
type: string
preview-url:
description: Preview deployment URL to invalidate for a named Neon branch
required: false
default: ''
type: string

permissions: {}

Expand Down Expand Up @@ -148,6 +175,10 @@ jobs:
PROTECTION_BYPASS_SECRET_STAGING: ${{ secrets.VERCEL_STAGING_BYPASS_SECRET }}
STAGING_SITE_URL: ${{ vars.STAGING_SITE_URL ||
'https://inferencemax-app-git-staging-semianalysisai.vercel.app' }}
NEON_API_KEY: ${{ secrets.NEON_API_KEY }}
NEON_PROJECT_ID: ${{ secrets.NEON_PROJECT_ID }}
NEON_BRANCH_NAME: ${{ inputs.neon-branch }}
PREVIEW_SITE_URL: ${{ inputs.preview-url }}
run: |
case "$REQUESTED_DATABASE_TARGET" in
production)
Expand All @@ -165,6 +196,29 @@ jobs:
cache_invalidate_url="${STAGING_SITE_URL%/}/api/v1/invalidate"
protection_bypass_secret="$PROTECTION_BYPASS_SECRET_STAGING"
;;
neon-branch)
if [ -z "$NEON_API_KEY" ] || [ -z "$NEON_PROJECT_ID" ] || [ -z "$NEON_BRANCH_NAME" ] || [ -z "$PREVIEW_SITE_URL" ]; then
echo "::error::NEON_API_KEY, NEON_PROJECT_ID, neon-branch, and preview-url are required"
exit 1
fi
branches=$(curl --retry 3 --retry-all-errors -sSf \
-H "Authorization: Bearer $NEON_API_KEY" \
"https://console.neon.tech/api/v2/projects/$NEON_PROJECT_ID/branches?limit=100")
branch_id=$(jq -r --arg name "$NEON_BRANCH_NAME" \
'.branches[] | select(.name == $name) | .id' <<<"$branches" | head -n 1)
if [ -z "$branch_id" ] || [ "$branch_id" = "null" ]; then
echo "::error::Neon branch not found: $NEON_BRANCH_NAME"
exit 1
fi
database_write_url=$(npx --yes neonctl connection-string \
--api-key "$NEON_API_KEY" \
--project-id "$NEON_PROJECT_ID" \
--branch-id "$branch_id" \
--database-name neondb \
--role-name neondb_owner)
cache_invalidate_url="${PREVIEW_SITE_URL%/}/api/v1/invalidate"
protection_bypass_secret="$PROTECTION_BYPASS_SECRET_STAGING"
;;
*)
echo "::error::Unsupported database-target: $REQUESTED_DATABASE_TARGET"
exit 1
Expand All @@ -175,12 +229,12 @@ jobs:
echo "::error::Database secret is empty for target: $REQUESTED_DATABASE_TARGET"
exit 1
fi
if [ "$REQUESTED_DATABASE_TARGET" != "staging" ] && [ -z "$cache_invalidate_secret" ]; then
if [ "$REQUESTED_DATABASE_TARGET" != "staging" ] && [ "$REQUESTED_DATABASE_TARGET" != "neon-branch" ] && [ -z "$cache_invalidate_secret" ]; then
echo "::error::Cache invalidation secret is empty for target: $REQUESTED_DATABASE_TARGET"
exit 1
fi
if [ "$REQUESTED_DATABASE_TARGET" = "staging" ] && [ -z "$protection_bypass_secret" ]; then
echo "::error::Vercel protection bypass secret is empty for staging"
if { [ "$REQUESTED_DATABASE_TARGET" = "staging" ] || [ "$REQUESTED_DATABASE_TARGET" = "neon-branch" ]; } && [ -z "$protection_bypass_secret" ]; then
echo "::error::Vercel protection bypass secret is empty for preview target"
exit 1
fi

Expand Down Expand Up @@ -234,7 +288,7 @@ jobs:

- name: Invalidate Vercel cache
run: |
if [ "$INGEST_DATABASE_TARGET" = "staging" ]; then
if [ "$INGEST_DATABASE_TARGET" = "staging" ] || [ "$INGEST_DATABASE_TARGET" = "neon-branch" ]; then
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
curl --retry 3 --retry-delay 2 --retry-connrefused -sSf -X POST "$CACHE_INVALIDATE_URL" \
-H "x-vercel-protection-bypass: $CACHE_PROTECTION_BYPASS_SECRET"
else
Expand Down
28 changes: 26 additions & 2 deletions .github/workflows/stage-results.yml
Original file line number Diff line number Diff line change
Expand Up @@ -265,10 +265,34 @@ jobs:
INFX_MAIN_PAT: ${{ secrets.INFX_MAIN_PAT }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

refresh-chart-series:
name: Refresh staged chart series
needs: [validate, ingest]
runs-on: blacksmith-16vcpu-ubuntu-2404
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version-file: package.json
- name: Install dependencies
run: bun install --frozen-lockfile
env:
CYPRESS_INSTALL_BINARY: '0'
- name: Recompute stale chart series for staged run
env:
DATABASE_WRITE_URL: ${{ secrets.DATABASE_STAGING_WRITE_URL }}
RUN_ID: ${{ needs.validate.outputs.run-id }}
run: bun run --cwd packages/db db:backfill-chart-series --run-id "$RUN_ID" --yes

report:
name: Report staging result
if: ${{ always() && needs.validate.result == 'success' }}
needs: [validate, sync-staging-branch, prepare-staging-database, ingest]
needs: [validate, sync-staging-branch, prepare-staging-database, ingest, refresh-chart-series]
runs-on: ubuntu-latest
permissions: {}
steps:
Expand All @@ -280,7 +304,7 @@ jobs:
PR_NUMBER: ${{ needs.validate.outputs.pr-number }}
REQUESTED_BY: ${{ needs.validate.outputs.requested-by }}
COMMENT_ID: ${{ needs.validate.outputs.comment-id }}
STAGE_SUCCEEDED: ${{ needs['sync-staging-branch'].result == 'success' && needs['prepare-staging-database'].result == 'success' && needs.ingest.result == 'success' }}
STAGE_SUCCEEDED: ${{ needs['sync-staging-branch'].result == 'success' && needs['prepare-staging-database'].result == 'success' && needs.ingest.result == 'success' && needs['refresh-chart-series'].result == 'success' }}
with:
github-token: ${{ secrets.PAT }}
script: |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { describe, expect, it } from 'vitest';

import {
LEGEND_ITEM_GAP,
LEGEND_ROW_HEIGHT,
LEGEND_TEXT_OFFSET,
estimateTextWidth,
layoutChartLegend,
} from './chart-legend';

// Inline render of the point-detail charts: 720 viewBox units wide with the
// shared 60/16 left/right axis padding.
const INLINE_INNER_WIDTH = 720 - 60 - 16;

/** Right edge of the item at `index`, i.e. where its text stops. */
const itemRight = (labels: readonly string[], index: number, width: number): number => {
const { items } = layoutChartLegend(labels, width);
return items[index]!.x + LEGEND_TEXT_OFFSET + estimateTextWidth(labels[index]!);
};

describe('estimateTextWidth', () => {
it('grows with the number of characters', () => {
expect(estimateTextWidth('decode')).toBeGreaterThan(estimateTextWidth('dec'));
});

it('counts CJK characters as roughly one em each', () => {
// Four fullwidth characters at font size 11.
expect(estimateTextWidth('芯片缓存')).toBeCloseTo(44, 5);
});

it('charges more for wide characters than narrow ones', () => {
expect(estimateTextWidth('mmmm')).toBeGreaterThan(estimateTextWidth('llll'));
});

it('returns zero for an empty label', () => {
expect(estimateTextWidth('')).toBe(0);
});

it('scales linearly with font size', () => {
expect(estimateTextWidth('decode', 22)).toBeCloseTo(estimateTextWidth('decode', 11) * 2, 5);
});
});

describe('layoutChartLegend', () => {
it('keeps a short legend on one row and reserves no extra height', () => {
const layout = layoutChartLegend(['Input', 'Decode'], INLINE_INNER_WIDTH);
expect(layout.rows).toBe(1);
expect(layout.extraHeight).toBe(0);
expect(layout.items.map((i) => i.row)).toEqual([0, 0]);
});

it('lays same-row items out left to right without overlapping', () => {
const labels = ['Input', 'Decode'];
const { items } = layoutChartLegend(labels, INLINE_INNER_WIDTH);
expect(items[0]!.x).toBe(0);
expect(items[1]!.x).toBe(LEGEND_TEXT_OFFSET + estimateTextWidth(labels[0]!) + LEGEND_ITEM_GAP);
expect(items[1]!.x).toBeGreaterThanOrEqual(itemRight(labels, 0, INLINE_INNER_WIDTH));
});

it('wraps the eleven-series KV-cache legend instead of overprinting it', () => {
// The regression from the inline KV-cache chart: eleven per-engine labels
// in ~644 units used to get 58 units each and collide.
const labels = [
'prefill (500e)',
'prefill (501a)',
'prefill (501e)',
'decode (5021)',
'decode (5023)',
'decode (5025)',
'decode (5027)',
'Chip HBM (avg n=50)',
'DRAM',
'CPU offload pool (avg n=50)',
'Avg',
];
const layout = layoutChartLegend(labels, INLINE_INNER_WIDTH);

expect(layout.rows).toBeGreaterThan(1);
expect(layout.extraHeight).toBe((layout.rows - 1) * LEGEND_ROW_HEIGHT);

for (const [i, label] of labels.entries()) {
const item = layout.items[i]!;
const right = item.x + LEGEND_TEXT_OFFSET + estimateTextWidth(label);
// Every item fits inside the plot width...
expect(right).toBeLessThanOrEqual(INLINE_INNER_WIDTH);
// ...and starts clear of its predecessor on the same row.
const prev = layout.items[i - 1];
if (prev && prev.row === item.row) {
expect(item.x).toBeGreaterThanOrEqual(
prev.x + LEGEND_TEXT_OFFSET + estimateTextWidth(labels[i - 1]!),
);
}
}
});

it('starts each wrapped row back at the left edge', () => {
const labels = Array.from({ length: 12 }, (_, i) => `series number ${i}`);
const layout = layoutChartLegend(labels, INLINE_INNER_WIDTH);
const firstOfRow = new Map<number, number>();
for (const item of layout.items) {
if (!firstOfRow.has(item.row)) firstOfRow.set(item.row, item.x);
}
for (const x of firstOfRow.values()) expect(x).toBe(0);
});

it('wraps sooner for Chinese labels, which are wider per character', () => {
const en = Array.from({ length: 6 }, () => 'Chip HBM pool');
const zh = Array.from({ length: 6 }, () => '芯片 HBM 显存池均值');
expect(layoutChartLegend(zh, INLINE_INNER_WIDTH).rows).toBeGreaterThanOrEqual(
layoutChartLegend(en, INLINE_INNER_WIDTH).rows,
);
});

it('needs fewer rows at the expanded width than inline', () => {
const labels = Array.from({ length: 11 }, (_, i) => `decode engine ${i}`);
const inline = layoutChartLegend(labels, INLINE_INNER_WIDTH);
const expanded = layoutChartLegend(labels, 1300 - 60 - 16);
expect(expanded.rows).toBeLessThan(inline.rows);
});

it('gives a label wider than the row its own row rather than dropping it', () => {
const layout = layoutChartLegend(['a', 'w'.repeat(300), 'b'], 100);
expect(layout.items).toHaveLength(3);
expect(layout.items[1]!.row).toBe(1);
expect(layout.items[1]!.x).toBe(0);
expect(layout.items[2]!.row).toBe(2);
});

it('reports a single row for an empty legend', () => {
const layout = layoutChartLegend([], INLINE_INNER_WIDTH);
expect(layout.items).toEqual([]);
expect(layout.rows).toBe(1);
expect(layout.extraHeight).toBe(0);
});
});
Loading
Loading