socket-function 0.8.37 → 0.8.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SocketFunction.ts +23 -6
- package/SocketFunctionTypes.ts +2 -0
- package/package.json +4 -2
- package/require/RequireController.ts +8 -2
- package/require/require.js +17 -26
- package/src/CallFactory.ts +69 -14
- package/src/JSONLACKS/JSONLACKS.generated.js +2202 -0
- package/src/JSONLACKS/JSONLACKS.generated.js.d.ts +1 -0
- package/src/JSONLACKS/JSONLACKS.pegjs +248 -0
- package/src/JSONLACKS/JSONLACKS.ts +376 -0
- package/src/batching.ts +127 -0
- package/src/caching.ts +7 -1
- package/src/callManager.ts +8 -5
- package/src/certStore.ts +2 -0
- package/src/fixLargeNetworkCalls.ts +9 -0
- package/src/formatting/colors.ts +79 -0
- package/src/formatting/format.ts +157 -0
- package/src/formatting/logColors.ts +18 -0
- package/src/misc.ts +74 -2
- package/src/nodeCache.ts +7 -4
- package/src/profiling/getOwnTime.ts +143 -0
- package/src/profiling/measure.ts +245 -0
- package/src/profiling/stats.ts +213 -0
- package/src/profiling/tcpLagProxy.ts +64 -0
- package/src/types.ts +1 -1
- package/src/webSocketServer.ts +24 -11
- package/src/websocketFactory.ts +7 -2
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import debugbreak from "debugbreak";
|
|
2
|
+
// TODO: We could probably make this an optional / dev dependency, to allow
|
|
3
|
+
// for use on machines without the ability to compile?
|
|
4
|
+
import { now } from "rdtsc-now";
|
|
5
|
+
|
|
6
|
+
export type OwnTimeObj = {
|
|
7
|
+
name: string;
|
|
8
|
+
time: number;
|
|
9
|
+
ownTime: number;
|
|
10
|
+
};
|
|
11
|
+
type OwnTimeObjInternal = OwnTimeObj & {
|
|
12
|
+
lastStartTime: number;
|
|
13
|
+
firstStartTime: number;
|
|
14
|
+
parent: OwnTimeObjInternal | undefined;
|
|
15
|
+
child: OwnTimeObjInternal | undefined;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
let pendingCallTime: OwnTimeObjInternal | undefined;
|
|
19
|
+
export function getPendingOwnTimeObjs(): (OwnTimeObj & { source: OwnTimeObjInternal })[] | undefined {
|
|
20
|
+
let time = now();
|
|
21
|
+
let instances = getPendingOwnTimeInstances();
|
|
22
|
+
if (!instances) return undefined;
|
|
23
|
+
if (!pendingCallTime) return undefined;
|
|
24
|
+
let results = instances.map((instance) => ({
|
|
25
|
+
name: instance.name,
|
|
26
|
+
ownTime: instance.ownTime,
|
|
27
|
+
time: time - instance.firstStartTime,
|
|
28
|
+
source: instance
|
|
29
|
+
}));
|
|
30
|
+
results[0].ownTime += time - pendingCallTime.lastStartTime;
|
|
31
|
+
return results;
|
|
32
|
+
}
|
|
33
|
+
export function getPendingOwnTimeInstances(): OwnTimeObjInternal[] | undefined {
|
|
34
|
+
if (!pendingCallTime) return undefined;
|
|
35
|
+
let results: OwnTimeObjInternal[] = [];
|
|
36
|
+
let current: OwnTimeObjInternal | undefined = pendingCallTime;
|
|
37
|
+
while (current) {
|
|
38
|
+
results.push(current);
|
|
39
|
+
current = current.parent;
|
|
40
|
+
}
|
|
41
|
+
return results;
|
|
42
|
+
}
|
|
43
|
+
(global as any).pendingOwnCallTime = pendingCallTime;
|
|
44
|
+
|
|
45
|
+
// NOTE: This overhead time is actually mostly for aggregate time, but it is needed,
|
|
46
|
+
// otherwise we consistently underestimate the time spent.
|
|
47
|
+
// ALSO! This forces high count lines to be at the top of the aggregate time list, which is really important!
|
|
48
|
+
// NOTE: The overhead time greatly varies, but even if it only takes 100ns, if 10X of that
|
|
49
|
+
// is significant, you are probably spending too much timing profiling anyway!
|
|
50
|
+
export const measureOverheadTime = 500 / 1000 / 1000;
|
|
51
|
+
// We internally add, because of where we measure time, there is time spent before we grab the
|
|
52
|
+
// current time, and after we record the last time, that is lost, but should be added.
|
|
53
|
+
let addMeasureOverheadTime = 0;
|
|
54
|
+
{
|
|
55
|
+
// NOTE: This is going to vary considerably. I assume because sometimes we are on a core
|
|
56
|
+
// that is free, and other times we are on a core that is hyperthreading with another hardware
|
|
57
|
+
// thread. This really hurts us because our timing uses rdtsc, which really hates hyper threading,
|
|
58
|
+
// and can easily get 50% slower because of it.
|
|
59
|
+
let results: number[] = [];
|
|
60
|
+
for (let j = 0; j < 10; j++) {
|
|
61
|
+
const measureCount = 1000 * 10;
|
|
62
|
+
let time = now();
|
|
63
|
+
for (let i = 0; i < measureCount; i++) {
|
|
64
|
+
getOwnTime("test", () => { }, () => { });
|
|
65
|
+
}
|
|
66
|
+
time = now() - time;
|
|
67
|
+
let overhead = time / measureCount;
|
|
68
|
+
results.push(overhead);
|
|
69
|
+
}
|
|
70
|
+
results.sort((a, b) => a - b);
|
|
71
|
+
addMeasureOverheadTime = results[results.length / 2];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// TIMING: About 60ns, of which 40ns is just now() calls.
|
|
75
|
+
// If async is closer to 300ns.
|
|
76
|
+
export function getOwnTime<T>(
|
|
77
|
+
name: string,
|
|
78
|
+
code: () => T,
|
|
79
|
+
onTime: (obj: OwnTimeObj) => void
|
|
80
|
+
): T {
|
|
81
|
+
let time = now();
|
|
82
|
+
let obj: OwnTimeObjInternal = {
|
|
83
|
+
name,
|
|
84
|
+
time: 0,
|
|
85
|
+
ownTime: 0,
|
|
86
|
+
firstStartTime: time,
|
|
87
|
+
lastStartTime: time,
|
|
88
|
+
parent: pendingCallTime,
|
|
89
|
+
child: undefined,
|
|
90
|
+
};
|
|
91
|
+
if (pendingCallTime) {
|
|
92
|
+
pendingCallTime.child = obj;
|
|
93
|
+
}
|
|
94
|
+
pendingCallTime = obj;
|
|
95
|
+
if (obj.parent) {
|
|
96
|
+
obj.parent.ownTime += obj.lastStartTime - obj.parent.lastStartTime;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function finish() {
|
|
100
|
+
let time = now();
|
|
101
|
+
obj.time = time - obj.firstStartTime;
|
|
102
|
+
if (pendingCallTime === obj) {
|
|
103
|
+
// Good case, all of our children call ended before us.
|
|
104
|
+
|
|
105
|
+
// End our own time calculation
|
|
106
|
+
obj.ownTime += time - obj.lastStartTime;
|
|
107
|
+
|
|
108
|
+
// Our parent is now the last open call
|
|
109
|
+
pendingCallTime = obj.parent;
|
|
110
|
+
if (pendingCallTime) {
|
|
111
|
+
// Resume our parent ownTime counting
|
|
112
|
+
pendingCallTime.lastStartTime = time;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (obj.child && obj.parent) {
|
|
116
|
+
obj.child.parent = obj.parent;
|
|
117
|
+
obj.parent.child = obj.child;
|
|
118
|
+
}
|
|
119
|
+
obj.parent = undefined;
|
|
120
|
+
obj.child = undefined;
|
|
121
|
+
|
|
122
|
+
obj.time += addMeasureOverheadTime;
|
|
123
|
+
obj.ownTime += addMeasureOverheadTime;
|
|
124
|
+
|
|
125
|
+
onTime(obj);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let isAsync = false;
|
|
129
|
+
try {
|
|
130
|
+
let result = code();
|
|
131
|
+
if (result && typeof result === "object" && result instanceof Promise) {
|
|
132
|
+
isAsync = true;
|
|
133
|
+
return result.finally(() => {
|
|
134
|
+
finish();
|
|
135
|
+
}) as any;
|
|
136
|
+
}
|
|
137
|
+
return result;
|
|
138
|
+
} finally {
|
|
139
|
+
if (!isAsync) {
|
|
140
|
+
finish();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import debugbreak from "debugbreak";
|
|
2
|
+
import { formatTime, formatNumber } from "../formatting/format";
|
|
3
|
+
import { red, yellow, blue } from "../formatting/logColors";
|
|
4
|
+
|
|
5
|
+
import { getOwnTime, getPendingOwnTimeInstances, getPendingOwnTimeObjs, OwnTimeObj } from "./getOwnTime";
|
|
6
|
+
import { addToStats, addToStatsValue, createStatsValue, getStatsTop, StatsValue } from "./stats";
|
|
7
|
+
|
|
8
|
+
/** NOTE: Must be called BEFORE anything else is imported! */
|
|
9
|
+
export function enableMeasurements() {
|
|
10
|
+
if (functionsSkipped) {
|
|
11
|
+
console.warn(red(`Skipped measure shimming ${functionsSkipped} functions. Fix this by calling enableMeasurements before any other imports.`));
|
|
12
|
+
}
|
|
13
|
+
measurementsEnabled = true;
|
|
14
|
+
}
|
|
15
|
+
/** NOTE: Must be called BEFORE anything else is imported! */
|
|
16
|
+
export function disableMeasurements() {
|
|
17
|
+
measurementsEnabled = false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let functionsSkipped = 0;
|
|
21
|
+
|
|
22
|
+
const measureOverhead = 5 / 1000;
|
|
23
|
+
|
|
24
|
+
const AsyncFunction = (async () => { }).constructor;
|
|
25
|
+
|
|
26
|
+
// TIMING: 1-5us. I have seen timing values greatly vary, but it does seem to be quite high, despite
|
|
27
|
+
// microbenchmarks saying it is slow. Perhaps it is because getOwnTime breaks the cpu pipeline,
|
|
28
|
+
// which causes slowness for code around us, but not if we are running in isolation?
|
|
29
|
+
export function measureFnc(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
|
30
|
+
let name = propertyKey;
|
|
31
|
+
if (target.name) {
|
|
32
|
+
name = `${target.name}.${name}`;
|
|
33
|
+
} else {
|
|
34
|
+
let constructorName = target.constructor.name;
|
|
35
|
+
if (constructorName) {
|
|
36
|
+
name = `${constructorName}().${name}`;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (descriptor.value instanceof AsyncFunction) {
|
|
40
|
+
name += `(async)`;
|
|
41
|
+
}
|
|
42
|
+
descriptor.value = measureWrap(descriptor.value, name);
|
|
43
|
+
}
|
|
44
|
+
export function measureWrap<T extends (...args: any[]) => any>(fnc: T, name?: string): T {
|
|
45
|
+
if (!measurementsEnabled) {
|
|
46
|
+
functionsSkipped++;
|
|
47
|
+
return fnc;
|
|
48
|
+
}
|
|
49
|
+
let usedName = name || fnc.name;
|
|
50
|
+
return (function (this: any, ...args: unknown[]): unknown {
|
|
51
|
+
if (outstandingProfiles.length === 0) {
|
|
52
|
+
return fnc.apply(this, args);
|
|
53
|
+
}
|
|
54
|
+
return getOwnTime(usedName, () => fnc.apply(this, args), recordOwnTime);
|
|
55
|
+
}) as T;
|
|
56
|
+
}
|
|
57
|
+
export function measureBlock<T extends (...args: any[]) => any>(fnc: T, name?: string): ReturnType<T> {
|
|
58
|
+
return measureWrap(fnc, name)();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function startMeasure(): {
|
|
62
|
+
finish: () => MeasureProfile;
|
|
63
|
+
} {
|
|
64
|
+
if (!measurementsEnabled) {
|
|
65
|
+
console.warn(red(`To capture measurements enableMeasurements() must be called before any other imports in your entry point`));
|
|
66
|
+
}
|
|
67
|
+
let profile: MeasureProfile = {
|
|
68
|
+
entries: Object.create(null),
|
|
69
|
+
};
|
|
70
|
+
let openAtStart = new Set(getPendingOwnTimeInstances());
|
|
71
|
+
|
|
72
|
+
outstandingProfiles.push(profile);
|
|
73
|
+
return {
|
|
74
|
+
finish() {
|
|
75
|
+
let pending = getPendingOwnTimeObjs() || [];
|
|
76
|
+
for (let timeObj of pending) {
|
|
77
|
+
if (openAtStart.has(timeObj.source)) continue;
|
|
78
|
+
addToProfile(profile, timeObj, true);
|
|
79
|
+
}
|
|
80
|
+
outstandingProfiles.splice(outstandingProfiles.indexOf(profile), 1);
|
|
81
|
+
return profile;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface LogMeasureTableConfig {
|
|
87
|
+
useTotalTime?: boolean;
|
|
88
|
+
name?: string;
|
|
89
|
+
// Defaults to 0.05
|
|
90
|
+
thresholdInTable?: number;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function logMeasureTable(
|
|
94
|
+
profile: MeasureProfile,
|
|
95
|
+
config?: LogMeasureTableConfig
|
|
96
|
+
) {
|
|
97
|
+
let { useTotalTime, name, thresholdInTable } = config || {};
|
|
98
|
+
if (thresholdInTable === undefined) {
|
|
99
|
+
thresholdInTable = 0.05;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getTime(entry: ProfileEntry) {
|
|
103
|
+
return useTotalTime ? entry.totalTime : entry.ownTime;
|
|
104
|
+
}
|
|
105
|
+
let entries = Object.values(profile.entries);
|
|
106
|
+
entries.sort((a, b) => getTime(b).sum - getTime(a).sum);
|
|
107
|
+
|
|
108
|
+
let totalTime = entries.map(x => getTime(x).sum).reduce((a, b) => a + b, 0);
|
|
109
|
+
|
|
110
|
+
console.log();
|
|
111
|
+
let title = yellow(`Profiled ${formatTime(totalTime)}`);
|
|
112
|
+
if (name) {
|
|
113
|
+
title += ` ${blue(name)}`;
|
|
114
|
+
}
|
|
115
|
+
console.log(title);
|
|
116
|
+
function percent(value: number) {
|
|
117
|
+
return `${(value * 100).toFixed(2)}%`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
entries = entries.slice(0, 10);
|
|
121
|
+
let maxNameLength = Math.max(...entries.map(x => x.name.length));
|
|
122
|
+
|
|
123
|
+
for (let entry of entries.slice(0, 10)) {
|
|
124
|
+
if (getTime(entry).sum / totalTime < thresholdInTable) break;
|
|
125
|
+
let output = "";
|
|
126
|
+
output += `${blue(entry.name)}`;
|
|
127
|
+
output += Array(maxNameLength + 4 - entry.name.length).fill(" ").join("");
|
|
128
|
+
|
|
129
|
+
let text = `${percent(getTime(entry).sum / totalTime).padStart(6, " ")} ( ${formatTime(getTime(entry).sum / getTime(entry).count).padStart(6, " ")} per * ${formatNumber(getTime(entry).count).padStart(6, " ")} = ${formatTime(getTime(entry).sum).padStart(6, " ")} )`;
|
|
130
|
+
let overhead = measureOverhead * getTime(entry).count;
|
|
131
|
+
let overheadFraction = overhead / getTime(entry).sum;
|
|
132
|
+
let overheadIsAProblem = overheadFraction > 0.5;
|
|
133
|
+
if (overheadIsAProblem) {
|
|
134
|
+
text = yellow(text);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
output += text;
|
|
138
|
+
|
|
139
|
+
if (overheadIsAProblem) {
|
|
140
|
+
output += red(` measurement overhead is ~${percent(overheadFraction)} of the time.`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let ownTimeTop = getStatsTop(getTime(entry));
|
|
144
|
+
if (ownTimeTop.topHeavy) {
|
|
145
|
+
output += red(` TOP ${percent(ownTimeTop.valueFraction)} of the time is owned by ${percent(ownTimeTop.countFraction)} of the calls`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (entry.stillOpenCount > 0) {
|
|
149
|
+
output += red(` (${entry.stillOpenCount} open)`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
console.log(output);
|
|
153
|
+
}
|
|
154
|
+
console.log();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function measureCode<T>(code: () => Promise<T>, config?: LogMeasureTableConfig) {
|
|
158
|
+
let measure = startMeasure();
|
|
159
|
+
try {
|
|
160
|
+
return await measureBlock(code, code.name || "untracked");
|
|
161
|
+
} finally {
|
|
162
|
+
finishProfile(measure, config);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
export function measureCodeSync<T>(code: () => T, config?: LogMeasureTableConfig): T {
|
|
166
|
+
let measure = startMeasure();
|
|
167
|
+
try {
|
|
168
|
+
return measureBlock(code, code.name || "untracked");
|
|
169
|
+
} finally {
|
|
170
|
+
finishProfile(measure, config);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function finishProfile(measure: { finish(): MeasureProfile }, config?: LogMeasureTableConfig) {
|
|
174
|
+
let profile = measure.finish();
|
|
175
|
+
logMeasureTable(profile, config);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
export interface MeasureProfile {
|
|
180
|
+
entries: {
|
|
181
|
+
[name: string]: ProfileEntry;
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
export function createMeasureProfile(): MeasureProfile {
|
|
185
|
+
return {
|
|
186
|
+
entries: Object.create(null),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function addToMeasureProfile(base: MeasureProfile, other: MeasureProfile) {
|
|
191
|
+
for (let name in other.entries) {
|
|
192
|
+
let entry = other.entries[name];
|
|
193
|
+
let baseEntry = base.entries[name];
|
|
194
|
+
if (!baseEntry) {
|
|
195
|
+
baseEntry = {
|
|
196
|
+
name: name,
|
|
197
|
+
ownTime: createStatsValue(),
|
|
198
|
+
totalTime: createStatsValue(),
|
|
199
|
+
stillOpenCount: 0,
|
|
200
|
+
};
|
|
201
|
+
base.entries[name] = baseEntry;
|
|
202
|
+
}
|
|
203
|
+
addToStats(baseEntry.ownTime, entry.ownTime);
|
|
204
|
+
addToStats(baseEntry.totalTime, entry.totalTime);
|
|
205
|
+
baseEntry.stillOpenCount += entry.stillOpenCount;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
interface ProfileEntry {
|
|
210
|
+
name: string;
|
|
211
|
+
ownTime: StatsValue;
|
|
212
|
+
totalTime: StatsValue;
|
|
213
|
+
stillOpenCount: number;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
let measurementsEnabled = true;
|
|
217
|
+
|
|
218
|
+
let outstandingProfiles: MeasureProfile[] = [];
|
|
219
|
+
function recordOwnTime(ownTimeObj: OwnTimeObj) {
|
|
220
|
+
if (outstandingProfiles.length === 0) return;
|
|
221
|
+
for (let i = 0; i < outstandingProfiles.length; i++) {
|
|
222
|
+
let profile = outstandingProfiles[i];
|
|
223
|
+
addToProfile(profile, ownTimeObj);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
function addToProfile(profile: MeasureProfile, ownTimeObj: OwnTimeObj, stillOpen?: boolean) {
|
|
229
|
+
let name = ownTimeObj.name;
|
|
230
|
+
let entry = profile.entries[name];
|
|
231
|
+
if (!entry) {
|
|
232
|
+
entry = {
|
|
233
|
+
name: name,
|
|
234
|
+
ownTime: createStatsValue(),
|
|
235
|
+
totalTime: createStatsValue(),
|
|
236
|
+
stillOpenCount: 0,
|
|
237
|
+
};
|
|
238
|
+
profile.entries[name] = entry;
|
|
239
|
+
}
|
|
240
|
+
addToStatsValue(entry.ownTime, ownTimeObj.ownTime);
|
|
241
|
+
addToStatsValue(entry.totalTime, ownTimeObj.time);
|
|
242
|
+
if (stillOpen) {
|
|
243
|
+
entry.stillOpenCount++;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
export interface StatsValue {
|
|
2
|
+
count: number;
|
|
3
|
+
sum: number;
|
|
4
|
+
sumSquares: number;
|
|
5
|
+
|
|
6
|
+
// All logs use base 10
|
|
7
|
+
// This supports values from 0.1ns to 1 week
|
|
8
|
+
logn7Value: number; logn7Count: number; logn6Value: number; logn6Count: number; logn5Value: number; logn5Count: number; logn4Value: number; logn4Count: number; logn3Value: number; logn3Count: number; logn2Value: number; logn2Count: number; logn1Value: number; logn1Count: number; log0Value: number; log0VCount: number; log1Value: number; log1VCount: number; log2Value: number; log2VCount: number; log3Value: number; log3VCount: number; log4Value: number; log4VCount: number; log5Value: number; log5VCount: number; log6Value: number; log6VCount: number; log7Value: number; log7VCount: number; log8Value: number; log8VCount: number; log9Value: number; log9VCount: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function createStatsValue(): StatsValue {
|
|
12
|
+
return {
|
|
13
|
+
count: 0,
|
|
14
|
+
sum: 0,
|
|
15
|
+
sumSquares: 0,
|
|
16
|
+
logn7Value: 0, logn7Count: 0, logn6Value: 0, logn6Count: 0, logn5Value: 0, logn5Count: 0, logn4Value: 0, logn4Count: 0, logn3Value: 0, logn3Count: 0, logn2Value: 0, logn2Count: 0, logn1Value: 0, logn1Count: 0, log0Value: 0, log0VCount: 0, log1Value: 0, log1VCount: 0, log2Value: 0, log2VCount: 0, log3Value: 0, log3VCount: 0, log4Value: 0, log4VCount: 0, log5Value: 0, log5VCount: 0, log6Value: 0, log6VCount: 0, log7Value: 0, log7VCount: 0, log8Value: 0, log8VCount: 0, log9Value: 0, log9VCount: 0,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// TIMING: Between 5.6ns and 8ns, depending on the size of the value (smaller values are faster to add)
|
|
21
|
+
// - A C++ implementation takes about 3.5ns, so... this is pretty fast!
|
|
22
|
+
export function addToStatsValue(stats: StatsValue, value: number) {
|
|
23
|
+
stats.count++;
|
|
24
|
+
stats.sum += value;
|
|
25
|
+
stats.sumSquares += value * value;
|
|
26
|
+
if (value < 0.000001) {
|
|
27
|
+
stats.logn7Value += value;
|
|
28
|
+
stats.logn7Count++;
|
|
29
|
+
} else if (value < 0.00001) {
|
|
30
|
+
stats.logn6Value += value;
|
|
31
|
+
stats.logn6Count++;
|
|
32
|
+
} else if (value < 0.0001) {
|
|
33
|
+
stats.logn5Value += value;
|
|
34
|
+
stats.logn5Count++;
|
|
35
|
+
} else if (value < 0.001) {
|
|
36
|
+
stats.logn4Value += value;
|
|
37
|
+
stats.logn4Count++;
|
|
38
|
+
} else if (value < 0.01) {
|
|
39
|
+
stats.logn3Value += value;
|
|
40
|
+
stats.logn3Count++;
|
|
41
|
+
} else if (value < 0.1) {
|
|
42
|
+
stats.logn2Value += value;
|
|
43
|
+
stats.logn2Count++;
|
|
44
|
+
} else if (value < 1) {
|
|
45
|
+
stats.logn1Value += value;
|
|
46
|
+
stats.logn1Count++;
|
|
47
|
+
} else if (value < 10) {
|
|
48
|
+
stats.log0Value += value;
|
|
49
|
+
stats.log0VCount++;
|
|
50
|
+
} else if (value < 100) {
|
|
51
|
+
stats.log1Value += value;
|
|
52
|
+
stats.log1VCount++;
|
|
53
|
+
} else if (value < 1000) {
|
|
54
|
+
stats.log2Value += value;
|
|
55
|
+
stats.log2VCount++;
|
|
56
|
+
} else if (value < 10000) {
|
|
57
|
+
stats.log3Value += value;
|
|
58
|
+
stats.log3VCount++;
|
|
59
|
+
} else if (value < 100000) {
|
|
60
|
+
stats.log4Value += value;
|
|
61
|
+
stats.log4VCount++;
|
|
62
|
+
} else if (value < 1000000) {
|
|
63
|
+
stats.log5Value += value;
|
|
64
|
+
stats.log5VCount++;
|
|
65
|
+
} else if (value < 10000000) {
|
|
66
|
+
stats.log6Value += value;
|
|
67
|
+
stats.log6VCount++;
|
|
68
|
+
} else if (value < 100000000) {
|
|
69
|
+
stats.log7Value += value;
|
|
70
|
+
stats.log7VCount++;
|
|
71
|
+
} else if (value < 1000000000) {
|
|
72
|
+
stats.log8Value += value;
|
|
73
|
+
stats.log8VCount++;
|
|
74
|
+
} else {
|
|
75
|
+
stats.log9Value += value;
|
|
76
|
+
stats.log9VCount++;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function addToStats(stats: StatsValue, other: StatsValue) {
|
|
81
|
+
stats.count += other.count;
|
|
82
|
+
stats.sum += other.sum;
|
|
83
|
+
stats.sumSquares += other.sumSquares;
|
|
84
|
+
stats.logn7Value += other.logn7Value;
|
|
85
|
+
stats.logn7Count += other.logn7Count;
|
|
86
|
+
stats.logn6Value += other.logn6Value;
|
|
87
|
+
stats.logn6Count += other.logn6Count;
|
|
88
|
+
stats.logn5Value += other.logn5Value;
|
|
89
|
+
stats.logn5Count += other.logn5Count;
|
|
90
|
+
stats.logn4Value += other.logn4Value;
|
|
91
|
+
stats.logn4Count += other.logn4Count;
|
|
92
|
+
stats.logn3Value += other.logn3Value;
|
|
93
|
+
stats.logn3Count += other.logn3Count;
|
|
94
|
+
stats.logn2Value += other.logn2Value;
|
|
95
|
+
stats.logn2Count += other.logn2Count;
|
|
96
|
+
stats.logn1Value += other.logn1Value;
|
|
97
|
+
stats.logn1Count += other.logn1Count;
|
|
98
|
+
stats.log0Value += other.log0Value;
|
|
99
|
+
stats.log0VCount += other.log0VCount;
|
|
100
|
+
stats.log1Value += other.log1Value;
|
|
101
|
+
stats.log1VCount += other.log1VCount;
|
|
102
|
+
stats.log2Value += other.log2Value;
|
|
103
|
+
stats.log2VCount += other.log2VCount;
|
|
104
|
+
stats.log3Value += other.log3Value;
|
|
105
|
+
stats.log3VCount += other.log3VCount;
|
|
106
|
+
stats.log4Value += other.log4Value;
|
|
107
|
+
stats.log4VCount += other.log4VCount;
|
|
108
|
+
stats.log5Value += other.log5Value;
|
|
109
|
+
stats.log5VCount += other.log5VCount;
|
|
110
|
+
stats.log6Value += other.log6Value;
|
|
111
|
+
stats.log6VCount += other.log6VCount;
|
|
112
|
+
stats.log7Value += other.log7Value;
|
|
113
|
+
stats.log7VCount += other.log7VCount;
|
|
114
|
+
stats.log8Value += other.log8Value;
|
|
115
|
+
stats.log8VCount += other.log8VCount;
|
|
116
|
+
stats.log9Value += other.log9Value;
|
|
117
|
+
stats.log9VCount += other.log9VCount;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
interface StatsBucket {
|
|
121
|
+
sum: number;
|
|
122
|
+
count: number;
|
|
123
|
+
}
|
|
124
|
+
// Ordered from lowest average to highest average
|
|
125
|
+
function getStatsBuckets(stats: StatsValue): StatsBucket[] {
|
|
126
|
+
return [
|
|
127
|
+
{ sum: stats.logn7Value, count: stats.logn7Count },
|
|
128
|
+
{ sum: stats.logn6Value, count: stats.logn6Count },
|
|
129
|
+
{ sum: stats.logn5Value, count: stats.logn5Count },
|
|
130
|
+
{ sum: stats.logn4Value, count: stats.logn4Count },
|
|
131
|
+
{ sum: stats.logn3Value, count: stats.logn3Count },
|
|
132
|
+
{ sum: stats.logn2Value, count: stats.logn2Count },
|
|
133
|
+
{ sum: stats.logn1Value, count: stats.logn1Count },
|
|
134
|
+
{ sum: stats.log0Value, count: stats.log0VCount },
|
|
135
|
+
{ sum: stats.log1Value, count: stats.log1VCount },
|
|
136
|
+
{ sum: stats.log2Value, count: stats.log2VCount },
|
|
137
|
+
{ sum: stats.log3Value, count: stats.log3VCount },
|
|
138
|
+
{ sum: stats.log4Value, count: stats.log4VCount },
|
|
139
|
+
{ sum: stats.log5Value, count: stats.log5VCount },
|
|
140
|
+
{ sum: stats.log6Value, count: stats.log6VCount },
|
|
141
|
+
{ sum: stats.log7Value, count: stats.log7VCount },
|
|
142
|
+
{ sum: stats.log8Value, count: stats.log8VCount },
|
|
143
|
+
{ sum: stats.log9Value, count: stats.log9VCount },
|
|
144
|
+
];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface StatsTop {
|
|
148
|
+
// NOTE: countFraction <= valueFraction, because we take the largest values
|
|
149
|
+
countFraction: number;
|
|
150
|
+
valueFraction: number;
|
|
151
|
+
count: number;
|
|
152
|
+
value: number;
|
|
153
|
+
topHeavy: boolean;
|
|
154
|
+
}
|
|
155
|
+
/** Identifies cases where the value is concentrated in few instances. This indicates most of the value (time)
|
|
156
|
+
* is not spent on the common case, but on an outlier. Which isn't a problem, it just means that the measurements
|
|
157
|
+
* should be more precise, to pull that heavy case out.
|
|
158
|
+
*/
|
|
159
|
+
export function getStatsTop(stats: StatsValue): StatsTop {
|
|
160
|
+
if (stats.sum === 0) {
|
|
161
|
+
return { countFraction: 1, valueFraction: 1, count: 0, value: 0, topHeavy: false, };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const minFraction = 0.2;
|
|
165
|
+
|
|
166
|
+
let totalSum = stats.sum;
|
|
167
|
+
let totalCount = stats.count;
|
|
168
|
+
let totalMean = totalSum / totalCount;
|
|
169
|
+
|
|
170
|
+
// Find the sum above the average (with a minimum of minFraction, as sometimes there isn't really anything above the average)
|
|
171
|
+
let buckets = getStatsBuckets(stats);
|
|
172
|
+
buckets.reverse();
|
|
173
|
+
|
|
174
|
+
let curSum = 0;
|
|
175
|
+
let curCount = 0;
|
|
176
|
+
for (let entry of buckets) {
|
|
177
|
+
let mean = entry.sum / entry.count;
|
|
178
|
+
if (curSum > totalSum * minFraction && mean < totalMean) break;
|
|
179
|
+
|
|
180
|
+
curSum += entry.sum;
|
|
181
|
+
curCount += entry.count;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let countFraction = curCount / totalCount;
|
|
185
|
+
let valueFraction = curSum / totalSum;
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
countFraction,
|
|
189
|
+
valueFraction,
|
|
190
|
+
count: curCount,
|
|
191
|
+
value: curSum,
|
|
192
|
+
// If more than 50% is above the average, it's top heavy
|
|
193
|
+
topHeavy: valueFraction / countFraction > 2 && valueFraction > 0.4,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
function benchmarkAddToStats() {
|
|
202
|
+
let stats = createStatsValue();
|
|
203
|
+
let start = Date.now();
|
|
204
|
+
const count = 1000000;
|
|
205
|
+
for (let i = 0; i < count; i++) {
|
|
206
|
+
addToStatsValue(stats, 10000000000);
|
|
207
|
+
}
|
|
208
|
+
let end = Date.now();
|
|
209
|
+
let time = end - start;
|
|
210
|
+
let timePer = (time) / count * 1000 * 1000;
|
|
211
|
+
console.log(`Time per: ${timePer}ns, in ${time}ms`);
|
|
212
|
+
}
|
|
213
|
+
//benchmarkAddToStats();
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import net from "net";
|
|
2
|
+
import { pipeline, PipelineTransform, PipelineTransformSource, Transform } from "stream";
|
|
3
|
+
|
|
4
|
+
export async function tcpLagProxy(config: {
|
|
5
|
+
localPort: number;
|
|
6
|
+
remoteHost: string;
|
|
7
|
+
remotePort: number;
|
|
8
|
+
// NOTE: Lag values between 1 and 10 are about the same, as setTimeout introduces a minimum delay.
|
|
9
|
+
// As a result lag values of 0 are about 2X faster than lag values of 1.
|
|
10
|
+
lag: number;
|
|
11
|
+
networkWriteSize?: { value: number };
|
|
12
|
+
networkReadSize?: { value: number };
|
|
13
|
+
networkWritePackets?: { value: number };
|
|
14
|
+
networkReadPackets?: { value: number };
|
|
15
|
+
}) {
|
|
16
|
+
const { localPort, remoteHost, remotePort, lag, networkWriteSize, networkReadSize, networkWritePackets, networkReadPackets } = config;
|
|
17
|
+
let server = net.createServer();
|
|
18
|
+
|
|
19
|
+
server.on("connection", async socket => {
|
|
20
|
+
// Swallow all errors, as the pipe should handle it anyways?
|
|
21
|
+
socket.on("error", () => { });
|
|
22
|
+
if (lag > 0) {
|
|
23
|
+
await new Promise(r => setTimeout(r, lag));
|
|
24
|
+
}
|
|
25
|
+
let remoteSocket = net.createConnection(remotePort, remoteHost);
|
|
26
|
+
remoteSocket.on("error", () => { });
|
|
27
|
+
|
|
28
|
+
const lagWrite = new Transform({
|
|
29
|
+
transform(chunk, encoding, callback) {
|
|
30
|
+
if (lag > 0) {
|
|
31
|
+
setTimeout(() => callback(undefined, chunk), lag);
|
|
32
|
+
} else {
|
|
33
|
+
callback(undefined, chunk);
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
const lagRead = new Transform({
|
|
38
|
+
transform(chunk, encoding, callback) {
|
|
39
|
+
if (lag > 0) {
|
|
40
|
+
setTimeout(() => callback(undefined, chunk), lag);
|
|
41
|
+
} else {
|
|
42
|
+
callback(undefined, chunk);
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
socket.pipe(lagWrite).pipe(remoteSocket);
|
|
48
|
+
remoteSocket.pipe(lagRead).pipe(socket);
|
|
49
|
+
|
|
50
|
+
socket.on("data", data => {
|
|
51
|
+
if (networkWriteSize) networkWriteSize.value += data.length;
|
|
52
|
+
if (networkWritePackets) networkWritePackets.value++;
|
|
53
|
+
});
|
|
54
|
+
remoteSocket.on("data", data => {
|
|
55
|
+
if (networkReadSize) networkReadSize.value += data.length;
|
|
56
|
+
if (networkReadPackets) networkReadPackets.value++;
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
server.listen(localPort);
|
|
60
|
+
return new Promise<void>((resolve, reject) => {
|
|
61
|
+
server.on("listening", () => resolve());
|
|
62
|
+
server.on("error", reject);
|
|
63
|
+
});
|
|
64
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -4,6 +4,6 @@ export type Args<T> = T extends (...args: infer V) => any ? V : never;
|
|
|
4
4
|
|
|
5
5
|
export type AnyFunction = (...args: any) => any;
|
|
6
6
|
|
|
7
|
-
export function canHaveChildren(value: unknown): value is
|
|
7
|
+
export function canHaveChildren(value: unknown): value is { [key in PropertyKey]: unknown } {
|
|
8
8
|
return typeof value === "object" && value !== null || typeof value === "function";
|
|
9
9
|
}
|