socket-function 0.8.37 → 0.8.38

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.
@@ -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,241 @@
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
+
16
+ let functionsSkipped = 0;
17
+
18
+ const measureOverhead = 5 / 1000;
19
+
20
+ const AsyncFunction = (async () => { }).constructor;
21
+
22
+ // TIMING: 1-5us. I have seen timing values greatly vary, but it does seem to be quite high, despite
23
+ // microbenchmarks saying it is slow. Perhaps it is because getOwnTime breaks the cpu pipeline,
24
+ // which causes slowness for code around us, but not if we are running in isolation?
25
+ export function measureFnc(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
26
+ let name = propertyKey;
27
+ if (target.name) {
28
+ name = `${target.name}.${name}`;
29
+ } else {
30
+ let constructorName = target.constructor.name;
31
+ if (constructorName) {
32
+ name = `${constructorName}().${name}`;
33
+ }
34
+ }
35
+ if (descriptor.value instanceof AsyncFunction) {
36
+ name += `(async)`;
37
+ }
38
+ descriptor.value = measureWrap(descriptor.value, name);
39
+ }
40
+ export function measureWrap<T extends (...args: any[]) => any>(fnc: T, name?: string): T {
41
+ if (!measurementsEnabled) {
42
+ functionsSkipped++;
43
+ return fnc;
44
+ }
45
+ let usedName = name || fnc.name;
46
+ return (function (this: any, ...args: unknown[]): unknown {
47
+ if (outstandingProfiles.length === 0) {
48
+ return fnc.apply(this, args);
49
+ }
50
+ return getOwnTime(usedName, () => fnc.apply(this, args), recordOwnTime);
51
+ }) as T;
52
+ }
53
+ export function measureBlock<T extends (...args: any[]) => any>(fnc: T, name?: string): ReturnType<T> {
54
+ return measureWrap(fnc, name)();
55
+ }
56
+
57
+ export function startMeasure(): {
58
+ finish: () => MeasureProfile;
59
+ } {
60
+ if (!measurementsEnabled) {
61
+ console.warn(red(`To capture measurements enableMeasurements() must be called before any other imports in your entry point`));
62
+ }
63
+ let profile: MeasureProfile = {
64
+ entries: Object.create(null),
65
+ };
66
+ let openAtStart = new Set(getPendingOwnTimeInstances());
67
+
68
+ outstandingProfiles.push(profile);
69
+ return {
70
+ finish() {
71
+ let pending = getPendingOwnTimeObjs() || [];
72
+ for (let timeObj of pending) {
73
+ if (openAtStart.has(timeObj.source)) continue;
74
+ addToProfile(profile, timeObj, true);
75
+ }
76
+ outstandingProfiles.splice(outstandingProfiles.indexOf(profile), 1);
77
+ return profile;
78
+ }
79
+ };
80
+ }
81
+
82
+ export interface LogMeasureTableConfig {
83
+ useTotalTime?: boolean;
84
+ name?: string;
85
+ // Defaults to 0.05
86
+ thresholdInTable?: number;
87
+ }
88
+
89
+ export function logMeasureTable(
90
+ profile: MeasureProfile,
91
+ config?: LogMeasureTableConfig
92
+ ) {
93
+ let { useTotalTime, name, thresholdInTable } = config || {};
94
+ if (thresholdInTable === undefined) {
95
+ thresholdInTable = 0.05;
96
+ }
97
+
98
+ function getTime(entry: ProfileEntry) {
99
+ return useTotalTime ? entry.totalTime : entry.ownTime;
100
+ }
101
+ let entries = Object.values(profile.entries);
102
+ entries.sort((a, b) => getTime(b).sum - getTime(a).sum);
103
+
104
+ let totalTime = entries.map(x => getTime(x).sum).reduce((a, b) => a + b, 0);
105
+
106
+ console.log();
107
+ let title = yellow(`Profiled ${formatTime(totalTime)}`);
108
+ if (name) {
109
+ title += ` ${blue(name)}`;
110
+ }
111
+ console.log(title);
112
+ function percent(value: number) {
113
+ return `${(value * 100).toFixed(2)}%`;
114
+ }
115
+
116
+ entries = entries.slice(0, 10);
117
+ let maxNameLength = Math.max(...entries.map(x => x.name.length));
118
+
119
+ for (let entry of entries.slice(0, 10)) {
120
+ if (getTime(entry).sum / totalTime < thresholdInTable) break;
121
+ let output = "";
122
+ output += `${blue(entry.name)}`;
123
+ output += Array(maxNameLength + 4 - entry.name.length).fill(" ").join("");
124
+
125
+ 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, " ")} )`;
126
+ let overhead = measureOverhead * getTime(entry).count;
127
+ let overheadFraction = overhead / getTime(entry).sum;
128
+ let overheadIsAProblem = overheadFraction > 0.5;
129
+ if (overheadIsAProblem) {
130
+ text = yellow(text);
131
+ }
132
+
133
+ output += text;
134
+
135
+ if (overheadIsAProblem) {
136
+ output += red(` measurement overhead is ~${percent(overheadFraction)} of the time.`);
137
+ }
138
+
139
+ let ownTimeTop = getStatsTop(getTime(entry));
140
+ if (ownTimeTop.topHeavy) {
141
+ output += red(` TOP ${percent(ownTimeTop.valueFraction)} of the time is owned by ${percent(ownTimeTop.countFraction)} of the calls`);
142
+ }
143
+
144
+ if (entry.stillOpenCount > 0) {
145
+ output += red(` (${entry.stillOpenCount} open)`);
146
+ }
147
+
148
+ console.log(output);
149
+ }
150
+ console.log();
151
+ }
152
+
153
+ export async function measureCode<T>(code: () => Promise<T>, config?: LogMeasureTableConfig) {
154
+ let measure = startMeasure();
155
+ try {
156
+ return await measureBlock(code, code.name || "untracked");
157
+ } finally {
158
+ finishProfile(measure, config);
159
+ }
160
+ }
161
+ export function measureCodeSync<T>(code: () => T, config?: LogMeasureTableConfig): T {
162
+ let measure = startMeasure();
163
+ try {
164
+ return measureBlock(code, code.name || "untracked");
165
+ } finally {
166
+ finishProfile(measure, config);
167
+ }
168
+ }
169
+ function finishProfile(measure: { finish(): MeasureProfile }, config?: LogMeasureTableConfig) {
170
+ let profile = measure.finish();
171
+ logMeasureTable(profile, config);
172
+ }
173
+
174
+
175
+ export interface MeasureProfile {
176
+ entries: {
177
+ [name: string]: ProfileEntry;
178
+ };
179
+ }
180
+ export function createMeasureProfile(): MeasureProfile {
181
+ return {
182
+ entries: Object.create(null),
183
+ };
184
+ }
185
+
186
+ export function addToMeasureProfile(base: MeasureProfile, other: MeasureProfile) {
187
+ for (let name in other.entries) {
188
+ let entry = other.entries[name];
189
+ let baseEntry = base.entries[name];
190
+ if (!baseEntry) {
191
+ baseEntry = {
192
+ name: name,
193
+ ownTime: createStatsValue(),
194
+ totalTime: createStatsValue(),
195
+ stillOpenCount: 0,
196
+ };
197
+ base.entries[name] = baseEntry;
198
+ }
199
+ addToStats(baseEntry.ownTime, entry.ownTime);
200
+ addToStats(baseEntry.totalTime, entry.totalTime);
201
+ baseEntry.stillOpenCount += entry.stillOpenCount;
202
+ }
203
+ }
204
+
205
+ interface ProfileEntry {
206
+ name: string;
207
+ ownTime: StatsValue;
208
+ totalTime: StatsValue;
209
+ stillOpenCount: number;
210
+ }
211
+
212
+ let measurementsEnabled = false;
213
+
214
+ let outstandingProfiles: MeasureProfile[] = [];
215
+ function recordOwnTime(ownTimeObj: OwnTimeObj) {
216
+ if (outstandingProfiles.length === 0) return;
217
+ for (let i = 0; i < outstandingProfiles.length; i++) {
218
+ let profile = outstandingProfiles[i];
219
+ addToProfile(profile, ownTimeObj);
220
+ }
221
+ }
222
+
223
+
224
+ function addToProfile(profile: MeasureProfile, ownTimeObj: OwnTimeObj, stillOpen?: boolean) {
225
+ let name = ownTimeObj.name;
226
+ let entry = profile.entries[name];
227
+ if (!entry) {
228
+ entry = {
229
+ name: name,
230
+ ownTime: createStatsValue(),
231
+ totalTime: createStatsValue(),
232
+ stillOpenCount: 0,
233
+ };
234
+ profile.entries[name] = entry;
235
+ }
236
+ addToStatsValue(entry.ownTime, ownTimeObj.ownTime);
237
+ addToStatsValue(entry.totalTime, ownTimeObj.time);
238
+ if (stillOpen) {
239
+ entry.stillOpenCount++;
240
+ }
241
+ }
@@ -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 object {
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
  }