-
Notifications
You must be signed in to change notification settings - Fork 20
/
load-gen.ts
223 lines (192 loc) · 7.89 KB
/
load-gen.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
import {
CacheClient,
DefaultMomentoLoggerFactory,
DefaultMomentoLoggerLevel,
MomentoLogger,
MomentoLoggerFactory,
} from '@gomomento/sdk';
import {range} from './utils/collections';
import {delay} from './utils/time';
import {
BasicLoadGenContext,
BasicLoadGenOptions,
executeRequestAndUpdateContextCounts,
initiateLoadGenContext,
} from './utils/load-gen';
import {getElapsedMillis, logStats} from './utils/load-gen-statistics-calculator';
import {ensureCacheExists, getCacheClient} from './utils/cache';
class BasicLoadGen {
private readonly loggerFactory: MomentoLoggerFactory;
private readonly logger: MomentoLogger;
private readonly cacheItemTtlSeconds = 60;
private readonly options: BasicLoadGenOptions;
private readonly delayMillisBetweenRequests: number;
private readonly cacheValue: string;
private readonly cacheName: string = 'js-loadgen';
constructor(options: BasicLoadGenOptions) {
this.loggerFactory = options.loggerFactory;
this.logger = this.loggerFactory.getLogger('load-gen');
this.options = options;
this.cacheValue = 'x'.repeat(options.cacheItemPayloadBytes);
this.delayMillisBetweenRequests =
(1000.0 * this.options.numberOfConcurrentRequests) / this.options.maxRequestsPerSecond;
}
async run(): Promise<void> {
const momento = await getCacheClient(this.loggerFactory, this.options.requestTimeoutMs, this.cacheItemTtlSeconds);
await ensureCacheExists(this.cacheName);
this.logger.trace(`delayMillisBetweenRequests: ${this.delayMillisBetweenRequests}`);
this.logger.info(`Limiting to ${this.options.maxRequestsPerSecond} tps`);
this.logger.info(`Running ${this.options.numberOfConcurrentRequests} concurrent requests`);
this.logger.info(`Running for ${this.options.totalSecondsToRun} seconds`);
const loadGenContext = initiateLoadGenContext();
const asyncGetSetResults = range(this.options.numberOfConcurrentRequests).map(workerId =>
this.launchAndRunWorkers(momento, loadGenContext, workerId + 1)
);
// Show stats periodically.
const logStatsIntervalId = setInterval(() => {
logStats(loadGenContext, this.logger, this.options.maxRequestsPerSecond);
}, this.options.showStatsIntervalSeconds * 1000);
await Promise.all(asyncGetSetResults);
// We're done, stop showing stats.
clearInterval(logStatsIntervalId);
// Show the stats one last time at the end.
logStats(loadGenContext, this.logger, this.options.maxRequestsPerSecond);
this.logger.info('DONE!');
// wait a few millis to allow the logger to finish flushing
await delay(500);
}
private async launchAndRunWorkers(
client: CacheClient,
loadGenContext: BasicLoadGenContext,
workerId: number
): Promise<void> {
let finished = false;
const finish = () => (finished = true);
setTimeout(finish, this.options.totalSecondsToRun * 1000);
let i = 1;
for (;;) {
await this.issueAsyncSetGet(client, loadGenContext, workerId, i);
if (finished) {
return;
}
i++;
}
}
private async issueAsyncSetGet(
client: CacheClient,
loadGenContext: BasicLoadGenContext,
workerId: number,
operationId: number
): Promise<void> {
const cacheKey = `worker${workerId}operation${operationId}`;
const setStartTime = process.hrtime();
const result = await executeRequestAndUpdateContextCounts(this.logger, loadGenContext, () =>
client.set(this.cacheName, cacheKey, this.cacheValue)
);
if (result !== undefined) {
const setDuration = getElapsedMillis(setStartTime);
loadGenContext.setLatencies.recordValue(setDuration);
if (setDuration < this.delayMillisBetweenRequests) {
const delayMs = this.delayMillisBetweenRequests - setDuration;
this.logger.trace(`delaying: ${delayMs}`);
await delay(delayMs);
}
}
const getStartTime = process.hrtime();
const getResult = await executeRequestAndUpdateContextCounts(this.logger, loadGenContext, () =>
client.get(this.cacheName, cacheKey)
);
if (getResult !== undefined) {
const getDuration = getElapsedMillis(getStartTime);
loadGenContext.getLatencies.recordValue(getDuration);
if (getDuration < this.delayMillisBetweenRequests) {
const delayMs = this.delayMillisBetweenRequests - getDuration;
this.logger.trace(`delaying: ${delayMs}`);
await delay(delayMs);
}
}
}
}
const PERFORMANCE_INFORMATION_MESSAGE = `
Thanks for trying out our basic node.js load generator! This tool is
included to allow you to experiment with performance in your environment
based on different configurations. It's very simplistic, and only intended
to give you a quick way to explore the performance of the Momento client
running on a single nodejs process.
Note that because nodejs javascript code runs on a single thread, the limiting
factor in request throughput will often be CPU. Keep an eye on your CPU
consumption while running the load generator, and if you reach 100%
of a CPU core then you most likely won't be able to improve throughput further
without running additional nodejs processes.
CPU will also impact your client-side latency; as you increase the number of
concurrent requests, if they are competing for CPU time then the observed
latency will increase.
Also, since performance will be impacted by network latency, you'll get the best
results if you run on a cloud VM in the same region as your Momento cache.
Check out the configuration settings at the bottom of the 'load-gen.ts' to
see how different configurations impact performance.
If you have questions or need help experimenting further, please reach out to us!
`;
async function main(loadGeneratorOptions: BasicLoadGenOptions) {
const loadGenerator = new BasicLoadGen(loadGeneratorOptions);
await loadGenerator.run();
}
const loadGeneratorOptions: BasicLoadGenOptions = {
/**
* This setting allows you to control the verbosity of the log output during
* the load generator run.
*/
loggerFactory: new DefaultMomentoLoggerFactory(
/**
* Available log levels are trace, debug, info, warn, and error.
*/
DefaultMomentoLoggerLevel.DEBUG
),
/** Print some statistics about throughput and latency every time this many
* seconds have passed.
*/
showStatsIntervalSeconds: 5,
/**
* Configures the Momento client to timeout if a request exceeds this limit.
* Momento client default is 5 seconds.
*/
requestTimeoutMs: 15 * 1000,
/**
* Controls the size of the payload that will be used for the cache items in
* the load test. Smaller payloads will generally provide lower latencies than
* larger payloads.
*/
cacheItemPayloadBytes: 100,
/**
* Sets an upper bound on how many requests per second will be sent to the server.
* Momento caches have a default throttling limit of 100 requests per second,
* so if you raise this, you may observe throttled requests. Contact
* support@momentohq.com to inquire about raising your limits.
*/
maxRequestsPerSecond: 100,
/**
* Controls the number of concurrent requests that will be made (via asynchronous
* function calls) by the load test. Increasing this number may improve throughput,
* but it will also increase CPU consumption. As CPU usage increases and there
* is more contention between the concurrent function calls, client-side latencies
* may increase.
*
* **Note**: You are likely to see degraded performance if you increase this above 50
* and observe elevated client-side latencies.
*/
numberOfConcurrentRequests: 10,
/**
* Controls how long the load test will run, in milliseconds. We will execute operations
* for this long and the exit.
*/
totalSecondsToRun: 60,
};
main(loadGeneratorOptions)
.then(() => {
console.log('success!!');
console.log(PERFORMANCE_INFORMATION_MESSAGE);
})
.catch((e: Error) => {
console.error(`Uncaught exception while running load gen: ${e.message}`);
throw e;
});