Skip to content

Commit 4eaf373

Browse files
thomasballingerConvex, Inc.
authored andcommitted
New ManyIntersections Load Generator workload (#39273)
GitOrigin-RevId: 313b5a1acdc42150d1390345085d511aa0b97fb3
1 parent 1426beb commit 4eaf373

File tree

10 files changed

+214
-4
lines changed

10 files changed

+214
-4
lines changed

crates/load_generator/src/main.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ enum Scenario {
133133
Search,
134134
VectorSearch,
135135
SnapshotExport,
136+
ManyIntersections { num_subscriptions: i32 },
136137
CloudBackup,
137138
RunHttpAction { path: String, method: String },
138139
}
@@ -151,6 +152,7 @@ impl Scenario {
151152
| Scenario::SnapshotExport
152153
| Scenario::CloudBackup
153154
| Scenario::RunHttpAction { .. } => false,
155+
Scenario::ManyIntersections { .. } => false,
154156
}
155157
}
156158

crates/load_generator/src/metrics.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,15 @@ static ERROR_METRICS: LazyLock<BTreeMap<&str, BTreeMap<&str, IntCounterVec>>> =
8888
)
8989
}
9090
},
91+
"ManyIntersections" => btreemap!{
92+
"mutation" => {
93+
register_convex_counter_owned!(
94+
MANY_INTERSECTIONS_INSERT_ERROR_TOTAL,
95+
"Errors on the insert mutation in many intersections scenario",
96+
&["backend_version", "load_description"],
97+
)
98+
}
99+
},
91100
"SnapshotExport" => btreemap!{
92101
"request_export_failed" => {
93102
register_convex_counter_owned!(
@@ -250,6 +259,22 @@ static LATENCY_METRICS: LazyLock<BTreeMap<&str, BTreeMap<&str, VMHistogramVec>>>
250259
)
251260
}
252261
},
262+
"ManyIntersections" => btreemap!{
263+
"mutation_completed" => {
264+
register_convex_histogram_owned!(
265+
OBSERVE_INSERT_WITH_MANY_INTERSECTIONS_COMPLETED_LATENCY_SECONDS,
266+
"Latency on mutation completion for ManyIntersections",
267+
&["backend_version", "load_description"],
268+
)
269+
},
270+
"mutation_observed" => {
271+
register_convex_histogram_owned!(
272+
OBSERVE_INSERT_WITH_MANY_INTERSECTIONS_OBSERVED_LATENCY_SECONDS,
273+
"Latency on observing mutation for ManyIntersections",
274+
&["backend_version", "load_description"],
275+
)
276+
},
277+
},
253278
"Search" => btreemap!{
254279
"search" => {
255280
register_convex_histogram_owned!(
@@ -331,6 +356,22 @@ static COUNT_METRICS: LazyLock<BTreeMap<&str, BTreeMap<&str, IntCounterVec>>> =
331356
)
332357
}
333358
},
359+
"ManyIntersections" => btreemap!{
360+
"mutation_send_timeout" => {
361+
register_convex_counter_owned!(
362+
OBSERVE_INSERT_WITH_MANY_INTERSECTIONS_SEND_TIMEOUT_TOTAL,
363+
"Count of send timeouts for mutation in ManyIntersections",
364+
&["backend_version", "load_description"],
365+
)
366+
},
367+
"mutation_observed_timeout" => {
368+
register_convex_counter_owned!(
369+
OBSERVE_INSERT_WITH_MANY_INTERSECTIONS_OBSERVED_TIMEOUT_TOTAL,
370+
"Count of observed timeouts for mutation in ManyIntersections",
371+
&["backend_version", "load_description"],
372+
)
373+
}
374+
},
334375
"ObserveInsertWithSearch" => btreemap!{
335376
"mutation_send_timeout" => {
336377
register_convex_counter_owned!(
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"name": "many_intersections",
3+
"scenarios": [
4+
{
5+
"name": "ManyIntersections",
6+
"benchmark": 1,
7+
"num_subscriptions": 20000
8+
}
9+
]
10+
}

npm-packages/@convex-dev/platform/src/generatedApi.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -144,13 +144,10 @@ export interface components {
144144
AdminKey: string;
145145
CreateProjectArgs: {
146146
deploymentType?: null | components["schemas"]["DeploymentType"];
147-
partitionId?: null | components["schemas"]["PartitionId"];
148147
projectName: components["schemas"]["ProjectName"];
149148
team: components["schemas"]["TeamSlug"];
150149
};
151150
ProjectName: string;
152-
/** Format: int64 */
153-
PartitionId: number;
154151
/** @enum {string} */
155152
DeploymentType: "dev" | "prod" | "preview";
156153
InstanceAuthForDashboardInteractionsResponse: {
@@ -259,7 +256,6 @@ export type ProjectId = components['schemas']['ProjectId'];
259256
export type AdminKey = components['schemas']['AdminKey'];
260257
export type CreateProjectArgs = components['schemas']['CreateProjectArgs'];
261258
export type ProjectName = components['schemas']['ProjectName'];
262-
export type PartitionId = components['schemas']['PartitionId'];
263259
export type DeploymentType = components['schemas']['DeploymentType'];
264260
export type InstanceAuthForDashboardInteractionsResponse = components['schemas']['InstanceAuthForDashboardInteractionsResponse'];
265261
export type SerializedAccessToken = components['schemas']['SerializedAccessToken'];

npm-packages/scenario-runner/convex/insert.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,31 @@ export const insertMessageWithArgs = mutation({
9797
);
9898
},
9999
});
100+
101+
export const insertMessagesWithArgs = mutation({
102+
args: {
103+
channel: v.string(),
104+
timestamp: v.number(),
105+
rand: v.number(),
106+
ballastCount: v.number(),
107+
count: v.number(),
108+
table: v.string(),
109+
n: v.number(),
110+
},
111+
handler: async (
112+
ctx,
113+
{ channel, timestamp, rand, ballastCount, count, table, n },
114+
) => {
115+
for (let i = 0; i < n; i++) {
116+
await insertMessageHelper(
117+
ctx.db,
118+
channel,
119+
timestamp,
120+
rand,
121+
ballastCount,
122+
count,
123+
table as TableNamesInDataModel<DataModel>,
124+
);
125+
}
126+
},
127+
});

npm-packages/scenario-runner/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { ConvexClient } from "convex/browser";
1313
import { ScenarioMessage } from "./types.js";
1414
import { RunHttpAction } from "./scenarios/run_http_action.js";
1515
import dns from "node:dns";
16+
import { ManyIntersections } from "./scenarios/many_intersections.js";
1617

1718
Sentry.init({
1819
tracesSampleRate: 0.1,
@@ -115,6 +116,9 @@ async function runScenario(
115116
case "ObserveInsert":
116117
scenario = new ObserveInsert(config, scenarioSpec.search_indexes);
117118
break;
119+
case "ManyIntersections":
120+
scenario = new ManyIntersections(config, scenarioSpec.num_subscriptions);
121+
break;
118122
case "SnapshotExport":
119123
scenario = new SnapshotExport(config, adminKey);
120124
break;

npm-packages/scenario-runner/metrics.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Metrics must first be added to crates/load_generator/src/metrics.rs
33
export type ScenarioName =
44
| "CloudBackup"
5+
| "ManyIntersections"
56
| "ObserveInsert"
67
| "ObserveInsertWithSearch"
78
| "RunFunction"
@@ -12,6 +13,9 @@ export type ScenarioName =
1213
export type ScenarioLatencyMetric =
1314
// CloudBackup
1415
| "backup"
16+
// ManyIntersections
17+
| "mutation_completed"
18+
| "mutation_observed"
1519
// ObserveInsert
1620
| "mutation_completed"
1721
| "mutation_observed"
@@ -33,6 +37,9 @@ export type ScenarioCountMetric =
3337
| "backup_completed"
3438
| "backup_timeout"
3539
| "request_backup_succeeded"
40+
// ManyIntersections
41+
| "mutation_observed_timeout"
42+
| "mutation_send_timeout"
3643
// ObserveInsert
3744
| "mutation_observed_timeout"
3845
| "mutation_send_timeout"
@@ -61,6 +68,8 @@ export type ScenarioError =
6168
| "backup_failure"
6269
| "get_backup_failed"
6370
| "request_backup_failed"
71+
// ManyIntersections
72+
| "mutation"
6473
// ObserveInsert
6574
| "mutation"
6675
// RunFunction

npm-packages/scenario-runner/scenario.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@ export abstract class Scenario {
5656
this.cleanUpFunctions = [];
5757
}
5858

59+
/**
60+
* Subscribe to a query and return a promise for the first result
61+
* received for which isReady(result) returns true.
62+
*
63+
* The subscription will remain active until the scenario ends but
64+
*/
5965
waitForQuery<Query extends FunctionReference<"query", "public">>(
6066
client: ConvexClient,
6167
query: Query,
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { MUTATION_TIMEOUT, QUERY_TIMEOUT } from "../types.js";
2+
import { Scenario, nowSeconds, IScenario, Config } from "../scenario.js";
3+
import { api } from "../convex/_generated/api.js";
4+
import { ScenarioError } from "../metrics.js";
5+
import { ConvexClient } from "convex/browser";
6+
import { MessagesTable, rand } from "../convex/common.js";
7+
import { writeFileSync } from "node:fs";
8+
9+
/**
10+
* Subscribe to many queries and invalidate a small number of them.
11+
*/
12+
export class ManyIntersections extends Scenario implements IScenario {
13+
table: MessagesTable;
14+
numSubscriptions: number;
15+
numDocumentsPerMutation: number;
16+
17+
constructor(config: Config, numSubscriptions = 1000) {
18+
const name = "ManyIntersections";
19+
super(name as any, config);
20+
this.table = "messages";
21+
this.numSubscriptions = numSubscriptions;
22+
this.numDocumentsPerMutation = 100;
23+
}
24+
25+
async sendMutation(client: ConvexClient, startTime: number, rand: number) {
26+
await client.mutation(api.insert.insertMessagesWithArgs, {
27+
channel: "global",
28+
timestamp: startTime,
29+
rand,
30+
ballastCount: 0,
31+
count: 1,
32+
table: this.table,
33+
n: this.numDocumentsPerMutation,
34+
});
35+
}
36+
37+
async run(client: ConvexClient) {
38+
writeFileSync(
39+
"/Users/tomb/log.txt",
40+
`Making ${this.numDocumentsPerMutation} document writes after ${this.numSubscriptions} subs at ${new Date().toTimeString().split(" ")[0]}...`,
41+
{
42+
encoding: "utf-8",
43+
},
44+
);
45+
const startTime = nowSeconds();
46+
const _ = range(this.numSubscriptions).map(() => {
47+
void this.subscribe(client, startTime, rand(), `channel-${rand()}`);
48+
});
49+
50+
// Wait a sec
51+
await new Promise((r) => setTimeout(r, 1000));
52+
53+
const t0 = nowSeconds();
54+
const randomNumber = rand();
55+
const subscribe = this.subscribe(client, startTime, randomNumber);
56+
57+
// Send the replace mutation or timeout
58+
await this.executeOrTimeoutWithLatency(
59+
this.sendMutation(client, startTime, randomNumber),
60+
MUTATION_TIMEOUT * 50,
61+
"mutation_send_timeout",
62+
"mutation_completed",
63+
t0,
64+
);
65+
66+
// Wait until the mutation is observed or timeout
67+
await this.executeOrTimeoutWithLatency(
68+
subscribe!,
69+
QUERY_TIMEOUT * 50,
70+
"mutation_observed_timeout",
71+
"mutation_observed",
72+
t0,
73+
);
74+
}
75+
76+
// Watch a query on the given offset of the table to observe the insert mutation
77+
async subscribe(
78+
client: ConvexClient,
79+
startTime: number,
80+
rand: number,
81+
channel = "global",
82+
): Promise<void> {
83+
await this.waitForQuery(
84+
client,
85+
api.query_index.queryMessagesWithArgs,
86+
{
87+
channel,
88+
rand,
89+
limit: 10,
90+
table: this.table,
91+
},
92+
(result) => {
93+
for (const doc of result || []) {
94+
if (doc.rand === rand && doc.timestamp! >= startTime) {
95+
return true;
96+
}
97+
}
98+
return false;
99+
},
100+
);
101+
}
102+
103+
defaultErrorName(): ScenarioError {
104+
return "mutation";
105+
}
106+
}
107+
108+
function range(n: number): number[] {
109+
return Array.from({ length: n }, (_, i) => i);
110+
}

npm-packages/scenario-runner/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ export type ScenarioSpec =
7373
name: "ObserveInsert";
7474
search_indexes: boolean;
7575
}
76+
| {
77+
name: "ManyIntersections";
78+
num_subscriptions: number;
79+
}
7680
| {
7781
name: "Search";
7882
}

0 commit comments

Comments
 (0)