windows-development-sandbox 1.27.1 → 1.28.0
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/dist/app/context.d.ts +5 -0
- package/dist/app/context.js +13 -0
- package/dist/app/features/accounts/accounts.def.d.ts +16 -0
- package/dist/app/features/accounts/accounts.run.d.ts +4 -0
- package/dist/app/features/accounts/services.def.d.ts +15 -1
- package/dist/app/features/accounts/services.def.js +67 -31
- package/dist/app/features/accounts/services.run.d.ts +7 -2
- package/dist/app/features/accounts/services.run.js +25 -6
- package/dist/app/features/defs.d.ts +17 -0
- package/dist/app/registry.d.ts +1 -1
- package/dist/app/registry.js +15 -2
- package/dist/bin/agent-sandbox.js +2 -2
- package/dist/bin/{chunk-RSIX7C6D.js → chunk-CB44HTTN.js} +444 -175
- package/dist/bin/{chunk-4TLSWPG7.js → chunk-UZ5S3EMB.js} +1 -1
- package/dist/bin/{chunk-QUHNXHVG.js → chunk-ZMZI3625.js} +46 -41
- package/dist/bin/claude-host.js +2 -2
- package/dist/bin/claude-sbx.js +2 -2
- package/dist/bin/codex-sbx.js +2 -2
- package/dist/bin/gemini-sbx.js +2 -2
- package/dist/bin/sandbox.js +2 -2
- package/dist/bin/sbx-hide.js +2 -2
- package/dist/bin/sbx-show.js +2 -2
- package/dist/cli/menu/menu.js +6 -1
- package/dist/cli/menu/pages/accounts-page.js +9 -5
- package/dist/core/schema/sandbox.js +1 -1
- package/dist/guest/apps/app-installer.js +7 -5
- package/dist/guest/apps/comet-unpack.js +2 -2
- package/dist/guest/daemon/heartbeat.js +1 -0
- package/dist/guest/governor/core.d.ts +53 -1
- package/dist/guest/governor/core.js +95 -1
- package/dist/guest/governor/decide.d.ts +4 -0
- package/dist/guest/governor/decide.js +2 -0
- package/dist/guest/governor/governor.js +23 -9
- package/dist/guest/runtime/pools.d.ts +4 -4
- package/dist/guest/runtime/pools.js +4 -15
- package/dist/guest/runtime/process.d.ts +5 -2
- package/dist/guest/runtime/process.js +23 -3
- package/dist/guest/toolchains/engines/epic-launcher.d.ts +6 -0
- package/dist/guest/toolchains/engines/epic-launcher.js +19 -1
- package/dist/guest/toolchains/engines/unreal-engines.js +2 -1
- package/dist/guest/toolchains/installers.d.ts +3 -3
- package/dist/guest/toolchains/installers.js +5 -5
- package/dist/guest/toolchains/native/unreal.js +22 -3
- package/dist/guest/toolchains/native/vcruntime.js +2 -2
- package/dist/guest/toolchains/native/vs-persist.d.ts +2 -2
- package/dist/guest/toolchains/runtimes.js +7 -6
- package/dist/guest/toolchains/signature.d.ts +2 -0
- package/dist/guest/toolchains/signature.js +9 -2
- package/dist/guest/toolchains/step-scheduler.js +1 -1
- package/dist/host/accounts/deliver.js +15 -7
- package/dist/host/life/crash-report.d.ts +4 -0
- package/dist/host/life/crash-report.js +30 -1
- package/dist/host/life/last-end.js +1 -0
- package/dist/host/pc-installs/discover.js +5 -2
- package/dist/host/pc-installs/unreal.d.ts +3 -1
- package/dist/host/pc-installs/unreal.js +27 -1
- package/dist/host/tools/windows-terminal.d.ts +4 -1
- package/dist/host/tools/windows-terminal.js +25 -9
- package/dist/runtime/agent.js +39 -23
- package/dist/runtime/daemon.js +279 -121
- package/dist/runtime/db.js +37 -17
- package/dist/runtime/main.js +340 -109
- package/dist/runtime/netproxy.js +31 -13
- package/dist/runtime/tool.js +37 -16
- package/dist/runtime/view.js +150 -38
- package/dist/selftest/agent-paths.test.js +3 -1
- package/dist/selftest/app.test.js +20 -2
- package/dist/selftest/boot-runner.test.js +15 -1
- package/dist/selftest/channel.test.js +33 -0
- package/dist/selftest/engines.test.js +22 -1
- package/dist/selftest/heavy-load.test.js +116 -4
- package/dist/selftest/live-terminal.test.js +103 -0
- package/dist/selftest/menu-screens.js +52 -1
- package/dist/selftest/menu.test.js +29 -3
- package/dist/selftest/personalization.test.js +2 -1
- package/dist/selftest/service-accounts.test.js +77 -2
- package/dist/selftest/support/engine-golden.js +2 -0
- package/dist/selftest/ui-text.test.js +4 -0
- package/dist/shared/accounts/kinds.js +9 -9
- package/dist/shared/apps/catalogue-gamedev.js +1 -0
- package/dist/shared/apps/catalogue.d.ts +2 -0
- package/dist/shared/channel/schema.d.ts +2 -0
- package/dist/shared/channel/schema.js +4 -0
- package/dist/shared/concurrency/pools.js +1 -1
- package/dist/shared/download/archive.js +4 -2
- package/dist/shared/download/index.d.ts +1 -1
- package/dist/shared/download/index.js +1 -1
- package/dist/shared/download/spawn.d.ts +7 -1
- package/dist/shared/download/spawn.js +16 -1
- package/dist/shared/memory/index.d.ts +1 -0
- package/dist/shared/memory/index.js +1 -0
- package/dist/shared/memory/policy.d.ts +12 -0
- package/dist/shared/memory/policy.js +4 -0
- package/dist/shared/memory/unreal-build.d.ts +2 -0
- package/dist/shared/memory/unreal-build.js +21 -0
- package/dist/shared/paths/pc-installs.d.ts +8 -0
- package/dist/shared/pty/clipboard-watch.d.ts +4 -0
- package/dist/shared/pty/clipboard-watch.js +40 -0
- package/dist/shared/pty/ctrl-c.d.ts +13 -7
- package/dist/shared/pty/ctrl-c.js +35 -19
- package/dist/shared/pty/index.d.ts +1 -0
- package/dist/shared/pty/index.js +1 -0
- package/dist/shared/pty/terminal.d.ts +1 -1
- package/dist/shared/pty/terminal.js +16 -13
- package/dist/shared/unreal/launcher-files.d.ts +7 -0
- package/dist/shared/unreal/launcher-files.js +32 -1
- package/dist/shared/web/ctrl-c.d.ts +26 -0
- package/dist/shared/web/ctrl-c.js +64 -0
- package/dist/shared/web/index.d.ts +1 -0
- package/dist/shared/web/index.js +1 -0
- package/dist/web/index.html +1 -1
- package/dist/web/{main-VPMYTSUL.js → main-7NY4X6YU.js} +67 -67
- package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
- package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
- package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
- package/package.json +1 -1
- package/sandbox/csharp/ClipboardWatch.cs +59 -0
- package/sandbox/csharp/MemGov.cs +752 -7
- package/sandbox/scripts/watch-clipboard.ps1 +8 -0
- package/schemas/config.schema.json +10 -10
- package/schemas/sandbox-config.schema.json +9 -9
package/sandbox/csharp/MemGov.cs
CHANGED
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
// stdout, a JSON line every --interval seconds (default 2):
|
|
5
5
|
// {"ramTotalMb":..,"ramAvailMb":..,"commitLimitMb":..,"commitAvailMb":..,"procs":[{"pid":..,"name":"rustc","mb":..,"prio":"normal"}]}
|
|
6
6
|
// procs = the processes in the agents' job (the job ptyhost makes for everything the agents start), nothing else.
|
|
7
|
+
// gateHeld = the build programs the memory gate holds right now (0 without a gate).
|
|
8
|
+
// serve [--interval s] [--gate growthMb:windowMs:releaseMs:maxHoldMs --gate-names a,b,c]
|
|
9
|
+
// With --gate, the memory gate (see GateLoop) also writes event lines on stdout:
|
|
10
|
+
// {"event":"gate","kind":"ready|off|trip|hold|release","at":<ms since 1970 UTC>, ...}
|
|
11
|
+
// Without --gate it never touches the job's completion port and writes no event.
|
|
7
12
|
// stdin, one command per line:
|
|
8
13
|
// priority <pid> <idle|below|normal> lowers (or restores) the priority of a process in that job
|
|
9
14
|
// iopriority <pid> <verylow|low|normal> the disk queue priority of a process in that job
|
|
@@ -51,6 +56,89 @@ static class MemGov
|
|
|
51
56
|
private const int IoPriorityVeryLow = 0;
|
|
52
57
|
private const int IoPriorityLow = 1;
|
|
53
58
|
private const int IoPriorityNormal = 2;
|
|
59
|
+
// The memory gate: a notification limit on the agents' job, reported through a completion port.
|
|
60
|
+
private const uint JobObjectSetAttributes = 0x0002;
|
|
61
|
+
private const int JobObjectAssociateCompletionPortInformation = 7;
|
|
62
|
+
private const int JobObjectNotificationLimitInformation = 12;
|
|
63
|
+
private const int JobObjectLimitViolationInformation = 13;
|
|
64
|
+
private const uint JobObjectLimitJobMemory = 0x00000200;
|
|
65
|
+
private const uint JobMsgNewProcess = 6;
|
|
66
|
+
private const uint JobMsgExitProcess = 7;
|
|
67
|
+
private const uint JobMsgAbnormalExitProcess = 8;
|
|
68
|
+
private const uint JobMsgNotificationLimit = 11;
|
|
69
|
+
private const uint GatePollMs = 100;
|
|
70
|
+
private const int GateTickMs = 500;
|
|
71
|
+
private const int GateRearmStepMb = 64;
|
|
72
|
+
private const int ErrorAccessDenied = 5;
|
|
73
|
+
private const int MaxTripList = 64;
|
|
74
|
+
private const int MaxGateNames = 64;
|
|
75
|
+
private static readonly IntPtr InvalidHandleValue = new IntPtr(-1);
|
|
76
|
+
private static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
|
77
|
+
|
|
78
|
+
// JOBOBJECT_ASSOCIATE_COMPLETION_PORT.
|
|
79
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
80
|
+
private struct CompletionPortLink
|
|
81
|
+
{
|
|
82
|
+
public IntPtr CompletionKey;
|
|
83
|
+
public IntPtr CompletionPort;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// JOBOBJECT_NOTIFICATION_LIMIT_INFORMATION (48 bytes on x64).
|
|
87
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
88
|
+
private struct NotificationLimit
|
|
89
|
+
{
|
|
90
|
+
public ulong IoReadBytesLimit;
|
|
91
|
+
public ulong IoWriteBytesLimit;
|
|
92
|
+
public long PerJobUserTimeLimit;
|
|
93
|
+
public ulong JobMemoryLimit;
|
|
94
|
+
public int RateControlTolerance;
|
|
95
|
+
public int RateControlToleranceInterval;
|
|
96
|
+
public uint LimitFlags;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// JOBOBJECT_LIMIT_VIOLATION_INFORMATION (80 bytes on x64).
|
|
100
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
101
|
+
private struct LimitViolation
|
|
102
|
+
{
|
|
103
|
+
public uint LimitFlags;
|
|
104
|
+
public uint ViolationLimitFlags;
|
|
105
|
+
public ulong IoReadBytes;
|
|
106
|
+
public ulong IoReadBytesLimit;
|
|
107
|
+
public ulong IoWriteBytes;
|
|
108
|
+
public ulong IoWriteBytesLimit;
|
|
109
|
+
public long PerJobUserTime;
|
|
110
|
+
public long PerJobUserTimeLimit;
|
|
111
|
+
public ulong JobMemory;
|
|
112
|
+
public ulong JobMemoryLimit;
|
|
113
|
+
public int RateControlTolerance;
|
|
114
|
+
public int RateControlToleranceLimit;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private sealed class GateSettings
|
|
118
|
+
{
|
|
119
|
+
public int GrowthMb;
|
|
120
|
+
public int WindowMs;
|
|
121
|
+
public int ReleaseMs;
|
|
122
|
+
public int MaxHoldMs;
|
|
123
|
+
public HashSet<string> Names;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/// <summary>A process the gate watches (or holds); the handle stays open while it is in Young or GateHeld.</summary>
|
|
127
|
+
private sealed class Tracked
|
|
128
|
+
{
|
|
129
|
+
public IntPtr Handle;
|
|
130
|
+
public string Name;
|
|
131
|
+
public DateTime Started;
|
|
132
|
+
public DateTime HeldAt;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private sealed class Released
|
|
136
|
+
{
|
|
137
|
+
public int Pid;
|
|
138
|
+
public string Name;
|
|
139
|
+
public long HeldMs;
|
|
140
|
+
public bool Forced;
|
|
141
|
+
}
|
|
54
142
|
|
|
55
143
|
// JOBOBJECT_CPU_RATE_CONTROL_INFORMATION (CpuRate in hundredths of a percent).
|
|
56
144
|
[StructLayout(LayoutKind.Sequential)]
|
|
@@ -103,6 +191,21 @@ static class MemGov
|
|
|
103
191
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
104
192
|
public static extern bool QueryInformationJobObject(IntPtr job, int infoClass, IntPtr info, int length, IntPtr returnLength);
|
|
105
193
|
|
|
194
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
195
|
+
public static extern bool QueryInformationJobObject(IntPtr job, int infoClass, ref LimitViolation info, int length, IntPtr returnLength);
|
|
196
|
+
|
|
197
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
198
|
+
public static extern IntPtr CreateIoCompletionPort(IntPtr file, IntPtr existing, UIntPtr key, uint threads);
|
|
199
|
+
|
|
200
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
201
|
+
public static extern bool GetQueuedCompletionStatus(IntPtr port, out uint bytes, out UIntPtr key, out IntPtr overlapped, uint ms);
|
|
202
|
+
|
|
203
|
+
[DllImport("kernel32.dll", EntryPoint = "SetInformationJobObject", SetLastError = true)]
|
|
204
|
+
public static extern bool SetInformationJobObject(IntPtr job, int infoClass, ref CompletionPortLink info, int length);
|
|
205
|
+
|
|
206
|
+
[DllImport("kernel32.dll", EntryPoint = "SetInformationJobObject", SetLastError = true)]
|
|
207
|
+
public static extern bool SetInformationJobObject(IntPtr job, int infoClass, ref NotificationLimit info, int length);
|
|
208
|
+
|
|
106
209
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
107
210
|
public static extern bool IsProcessInJob(IntPtr process, IntPtr job, out bool result);
|
|
108
211
|
|
|
@@ -162,20 +265,41 @@ static class MemGov
|
|
|
162
265
|
private static IntPtr throttleJob = IntPtr.Zero;
|
|
163
266
|
private static volatile bool stop;
|
|
164
267
|
|
|
268
|
+
// The memory gate (null gate = off). Young, GateHeld and the ledger Paused are guarded by Gate; the rest belongs to GateLoop.
|
|
269
|
+
private static readonly object OutLock = new object();
|
|
270
|
+
private static GateSettings gate = null;
|
|
271
|
+
private static readonly Dictionary<int, Tracked> Young = new Dictionary<int, Tracked>();
|
|
272
|
+
private static readonly Dictionary<int, Tracked> GateHeld = new Dictionary<int, Tracked>();
|
|
273
|
+
private static List<KeyValuePair<DateTime, long>> window = new List<KeyValuePair<DateTime, long>>();
|
|
274
|
+
private static DateTime lastTrip = DateTime.MinValue;
|
|
275
|
+
private static DateTime lastRelease = DateTime.MinValue;
|
|
276
|
+
private static long armedMb = -1;
|
|
277
|
+
private static HashSet<int> seenPids = new HashSet<int>();
|
|
278
|
+
|
|
165
279
|
static int Main(string[] args)
|
|
166
280
|
{
|
|
167
281
|
if (args.Length == 0 || args[0] != "serve")
|
|
168
282
|
{
|
|
169
|
-
Console.Error.WriteLine("usage: memgov serve [--interval seconds]");
|
|
283
|
+
Console.Error.WriteLine("usage: memgov serve [--interval seconds] [--gate growthMb:windowMs:releaseMs:maxHoldMs --gate-names a,b,c]");
|
|
170
284
|
return 2;
|
|
171
285
|
}
|
|
172
286
|
int interval = 2;
|
|
287
|
+
string gateSpec = null;
|
|
288
|
+
string gateNames = null;
|
|
173
289
|
for (int i = 1; i + 1 < args.Length; i++)
|
|
174
290
|
{
|
|
175
291
|
if (args[i] == "--interval")
|
|
176
292
|
{
|
|
177
293
|
int.TryParse(args[i + 1], NumberStyles.Integer, CultureInfo.InvariantCulture, out interval);
|
|
178
294
|
}
|
|
295
|
+
else if (args[i] == "--gate")
|
|
296
|
+
{
|
|
297
|
+
gateSpec = args[i + 1];
|
|
298
|
+
}
|
|
299
|
+
else if (args[i] == "--gate-names")
|
|
300
|
+
{
|
|
301
|
+
gateNames = args[i + 1];
|
|
302
|
+
}
|
|
179
303
|
}
|
|
180
304
|
if (interval < 1)
|
|
181
305
|
{
|
|
@@ -183,6 +307,21 @@ static class MemGov
|
|
|
183
307
|
}
|
|
184
308
|
// A memgov that ended without resuming (killed, crashed) left its pauses in the file: undo them first.
|
|
185
309
|
RecoverHeld();
|
|
310
|
+
Thread gateThread = null;
|
|
311
|
+
if (gateSpec != null)
|
|
312
|
+
{
|
|
313
|
+
gate = ParseGate(gateSpec, gateNames);
|
|
314
|
+
if (gate == null)
|
|
315
|
+
{
|
|
316
|
+
EmitOff("bad --gate");
|
|
317
|
+
}
|
|
318
|
+
else
|
|
319
|
+
{
|
|
320
|
+
gateThread = new Thread(GateLoop);
|
|
321
|
+
gateThread.IsBackground = true;
|
|
322
|
+
gateThread.Start();
|
|
323
|
+
}
|
|
324
|
+
}
|
|
186
325
|
// A memgov that ended while compilers were capped left the cap: lift it before anything else.
|
|
187
326
|
SetCpuRate(100);
|
|
188
327
|
Thread commands = new Thread(ReadCommands);
|
|
@@ -195,8 +334,7 @@ static class MemGov
|
|
|
195
334
|
{
|
|
196
335
|
try
|
|
197
336
|
{
|
|
198
|
-
|
|
199
|
-
Console.Out.Flush();
|
|
337
|
+
Emit(Sample());
|
|
200
338
|
}
|
|
201
339
|
catch (Exception)
|
|
202
340
|
{
|
|
@@ -207,11 +345,26 @@ static class MemGov
|
|
|
207
345
|
Thread.Sleep(100);
|
|
208
346
|
}
|
|
209
347
|
}
|
|
348
|
+
if (gateThread != null)
|
|
349
|
+
{
|
|
350
|
+
// The gate lifts its limit and closes its port when it sees stop; give it a moment.
|
|
351
|
+
gateThread.Join(3000);
|
|
352
|
+
}
|
|
210
353
|
ResumeAll();
|
|
211
354
|
SetCpuRate(100);
|
|
212
355
|
return 0;
|
|
213
356
|
}
|
|
214
357
|
|
|
358
|
+
/// <summary>One line on stdout; the sample loop and the gate both write, so one at a time.</summary>
|
|
359
|
+
private static void Emit(string json)
|
|
360
|
+
{
|
|
361
|
+
lock (OutLock)
|
|
362
|
+
{
|
|
363
|
+
Console.Out.WriteLine(json);
|
|
364
|
+
Console.Out.Flush();
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
215
368
|
/// <summary>The self-test's own job (MEMGOV_JOB_NAME, a name in the agents' job family), or null.</summary>
|
|
216
369
|
private static string TestJobName()
|
|
217
370
|
{
|
|
@@ -269,7 +422,12 @@ static class MemGov
|
|
|
269
422
|
first = false;
|
|
270
423
|
sb.Append(entry);
|
|
271
424
|
}
|
|
272
|
-
|
|
425
|
+
int heldNow;
|
|
426
|
+
lock (Gate)
|
|
427
|
+
{
|
|
428
|
+
heldNow = GateHeld.Count;
|
|
429
|
+
}
|
|
430
|
+
return sb.Append("],\"gateHeld\":").Append(heldNow.ToString(CultureInfo.InvariantCulture)).Append('}').ToString();
|
|
273
431
|
}
|
|
274
432
|
|
|
275
433
|
private static readonly int SelfId = Process.GetCurrentProcess().Id;
|
|
@@ -567,7 +725,7 @@ static class MemGov
|
|
|
567
725
|
{
|
|
568
726
|
lock (Gate)
|
|
569
727
|
{
|
|
570
|
-
if (Paused.ContainsKey(pid))
|
|
728
|
+
if (Paused.ContainsKey(pid) || GateHeld.ContainsKey(pid))
|
|
571
729
|
{
|
|
572
730
|
return;
|
|
573
731
|
}
|
|
@@ -638,16 +796,27 @@ static class MemGov
|
|
|
638
796
|
Resume(pid);
|
|
639
797
|
}
|
|
640
798
|
Paused.Clear();
|
|
799
|
+
foreach (Tracked t in GateHeld.Values)
|
|
800
|
+
{
|
|
801
|
+
NativeMethods.NtResumeProcess(t.Handle);
|
|
802
|
+
NativeMethods.CloseHandle(t.Handle);
|
|
803
|
+
}
|
|
804
|
+
GateHeld.Clear();
|
|
805
|
+
foreach (Tracked t in Young.Values)
|
|
806
|
+
{
|
|
807
|
+
NativeMethods.CloseHandle(t.Handle);
|
|
808
|
+
}
|
|
809
|
+
Young.Clear();
|
|
641
810
|
SaveHeld();
|
|
642
811
|
}
|
|
643
812
|
}
|
|
644
813
|
|
|
645
|
-
/// <summary>The processes paused right now, one id per line (empty: the file goes). Called with Gate held.</summary>
|
|
814
|
+
/// <summary>The processes paused or held by the gate right now, one id per line (empty: the file goes). Called with Gate held.</summary>
|
|
646
815
|
private static void SaveHeld()
|
|
647
816
|
{
|
|
648
817
|
try
|
|
649
818
|
{
|
|
650
|
-
if (Paused.Count == 0)
|
|
819
|
+
if (Paused.Count == 0 && GateHeld.Count == 0)
|
|
651
820
|
{
|
|
652
821
|
File.Delete(HeldFile);
|
|
653
822
|
return;
|
|
@@ -657,6 +826,13 @@ static class MemGov
|
|
|
657
826
|
{
|
|
658
827
|
lines.Add(pid.ToString(CultureInfo.InvariantCulture));
|
|
659
828
|
}
|
|
829
|
+
foreach (int pid in GateHeld.Keys)
|
|
830
|
+
{
|
|
831
|
+
if (!Paused.ContainsKey(pid))
|
|
832
|
+
{
|
|
833
|
+
lines.Add(pid.ToString(CultureInfo.InvariantCulture));
|
|
834
|
+
}
|
|
835
|
+
}
|
|
660
836
|
File.WriteAllLines(HeldFile, lines.ToArray());
|
|
661
837
|
}
|
|
662
838
|
catch (IOException)
|
|
@@ -698,6 +874,575 @@ static class MemGov
|
|
|
698
874
|
}
|
|
699
875
|
}
|
|
700
876
|
|
|
877
|
+
// ---- The memory gate -------------------------------------------------------------------------------------------
|
|
878
|
+
// When the agents' programs gain committed memory faster than GrowthMb per WindowMs, build programs that start
|
|
879
|
+
// (or just started) are frozen and let go one at a time, oldest first, never longer than MaxHoldMs. Windows tells
|
|
880
|
+
// us through a notification limit on the job (a completion port); nothing is ever killed.
|
|
881
|
+
|
|
882
|
+
/// <summary>growthMb:windowMs:releaseMs:maxHoldMs and the names the gate may freeze; null when either is invalid.</summary>
|
|
883
|
+
private static GateSettings ParseGate(string spec, string names)
|
|
884
|
+
{
|
|
885
|
+
string[] parts = spec.Split(':');
|
|
886
|
+
if (parts.Length != 4 || names == null)
|
|
887
|
+
{
|
|
888
|
+
return null;
|
|
889
|
+
}
|
|
890
|
+
int[] v = new int[4];
|
|
891
|
+
for (int i = 0; i < 4; i++)
|
|
892
|
+
{
|
|
893
|
+
if (!int.TryParse(parts[i], NumberStyles.Integer, CultureInfo.InvariantCulture, out v[i]))
|
|
894
|
+
{
|
|
895
|
+
return null;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
if (v[0] < 64 || v[1] < 500 || v[1] > 60000 || v[2] < 100 || v[2] > 60000 || v[3] < 1000 || v[3] > MaxSuspendMs)
|
|
899
|
+
{
|
|
900
|
+
return null;
|
|
901
|
+
}
|
|
902
|
+
HashSet<string> set = new HashSet<string>();
|
|
903
|
+
foreach (string raw in names.Split(','))
|
|
904
|
+
{
|
|
905
|
+
string name = raw.Trim().ToLowerInvariant();
|
|
906
|
+
if (name.Length > 0)
|
|
907
|
+
{
|
|
908
|
+
set.Add(name);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
if (set.Count == 0 || set.Count > MaxGateNames)
|
|
912
|
+
{
|
|
913
|
+
return null;
|
|
914
|
+
}
|
|
915
|
+
GateSettings g = new GateSettings();
|
|
916
|
+
g.GrowthMb = v[0];
|
|
917
|
+
g.WindowMs = v[1];
|
|
918
|
+
g.ReleaseMs = v[2];
|
|
919
|
+
g.MaxHoldMs = v[3];
|
|
920
|
+
g.Names = set;
|
|
921
|
+
return g;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
private static string EventStart(string kind)
|
|
925
|
+
{
|
|
926
|
+
long at = (long)(DateTime.UtcNow - Epoch).TotalMilliseconds;
|
|
927
|
+
return "{\"event\":\"gate\",\"kind\":\"" + kind + "\",\"at\":" + Num(at);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
private static string Num(long value)
|
|
931
|
+
{
|
|
932
|
+
return value.ToString(CultureInfo.InvariantCulture);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
private static void EmitOff(string reason)
|
|
936
|
+
{
|
|
937
|
+
if (reason.Length > 150)
|
|
938
|
+
{
|
|
939
|
+
reason = reason.Substring(0, 150);
|
|
940
|
+
}
|
|
941
|
+
Emit(EventStart("off") + ",\"reason\":" + Json(reason) + "}");
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
/// <summary>Opens the job, joins its completion port, then reacts to births, exits and notifications until stop.</summary>
|
|
945
|
+
private static void GateLoop()
|
|
946
|
+
{
|
|
947
|
+
IntPtr h = IntPtr.Zero;
|
|
948
|
+
IntPtr port = IntPtr.Zero;
|
|
949
|
+
try
|
|
950
|
+
{
|
|
951
|
+
// The job does not exist until the first ptyhost makes it.
|
|
952
|
+
while (!stop)
|
|
953
|
+
{
|
|
954
|
+
h = NativeMethods.OpenJobObjectW(JobObjectQuery | JobObjectSetAttributes, false, JobName());
|
|
955
|
+
if (h != IntPtr.Zero)
|
|
956
|
+
{
|
|
957
|
+
break;
|
|
958
|
+
}
|
|
959
|
+
if (Marshal.GetLastWin32Error() == ErrorAccessDenied)
|
|
960
|
+
{
|
|
961
|
+
EmitOff("cannot open the agents' job (access denied)");
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
Thread.Sleep(500);
|
|
965
|
+
}
|
|
966
|
+
if (h == IntPtr.Zero)
|
|
967
|
+
{
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
bool notify = true;
|
|
971
|
+
port = NativeMethods.CreateIoCompletionPort(InvalidHandleValue, IntPtr.Zero, UIntPtr.Zero, 1);
|
|
972
|
+
if (port == IntPtr.Zero)
|
|
973
|
+
{
|
|
974
|
+
notify = false;
|
|
975
|
+
}
|
|
976
|
+
else
|
|
977
|
+
{
|
|
978
|
+
// First drop a dead memgov's port (one port per job), then take the job over.
|
|
979
|
+
SetPortLink(h, IntPtr.Zero, IntPtr.Zero);
|
|
980
|
+
if (!SetPortLink(h, new IntPtr(1), port))
|
|
981
|
+
{
|
|
982
|
+
NativeMethods.CloseHandle(port);
|
|
983
|
+
port = IntPtr.Zero;
|
|
984
|
+
notify = false;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
DateTime now = DateTime.UtcNow;
|
|
988
|
+
long jobMb = JobCommitMb();
|
|
989
|
+
window = new List<KeyValuePair<DateTime, long>>();
|
|
990
|
+
window.Add(new KeyValuePair<DateTime, long>(now, jobMb));
|
|
991
|
+
if (notify && !SetLimit(h, jobMb + gate.GrowthMb))
|
|
992
|
+
{
|
|
993
|
+
notify = false;
|
|
994
|
+
}
|
|
995
|
+
if (!notify)
|
|
996
|
+
{
|
|
997
|
+
seenPids = new HashSet<int>(JobProcessIds());
|
|
998
|
+
}
|
|
999
|
+
Emit(EventStart("ready") + ",\"mode\":\"" + (notify ? "notify" : "poll") + "\",\"growthMb\":" + Num(gate.GrowthMb)
|
|
1000
|
+
+ ",\"windowMs\":" + Num(gate.WindowMs) + "}");
|
|
1001
|
+
DateTime lastTick = DateTime.UtcNow;
|
|
1002
|
+
while (!stop)
|
|
1003
|
+
{
|
|
1004
|
+
if (notify)
|
|
1005
|
+
{
|
|
1006
|
+
uint msg;
|
|
1007
|
+
UIntPtr key;
|
|
1008
|
+
IntPtr overlapped;
|
|
1009
|
+
if (NativeMethods.GetQueuedCompletionStatus(port, out msg, out key, out overlapped, GatePollMs))
|
|
1010
|
+
{
|
|
1011
|
+
int pid = (int)overlapped.ToInt64();
|
|
1012
|
+
if (msg == JobMsgNewProcess)
|
|
1013
|
+
{
|
|
1014
|
+
OnBirth(pid);
|
|
1015
|
+
}
|
|
1016
|
+
else if (msg == JobMsgExitProcess || msg == JobMsgAbnormalExitProcess)
|
|
1017
|
+
{
|
|
1018
|
+
OnExit(pid);
|
|
1019
|
+
}
|
|
1020
|
+
else if (msg == JobMsgNotificationLimit)
|
|
1021
|
+
{
|
|
1022
|
+
OnNotified(h, pid);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
else
|
|
1027
|
+
{
|
|
1028
|
+
Thread.Sleep((int)GatePollMs);
|
|
1029
|
+
}
|
|
1030
|
+
if ((DateTime.UtcNow - lastTick).TotalMilliseconds >= GateTickMs)
|
|
1031
|
+
{
|
|
1032
|
+
lastTick = DateTime.UtcNow;
|
|
1033
|
+
Tick(h, notify);
|
|
1034
|
+
}
|
|
1035
|
+
ReleaseDue();
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
catch (Exception e)
|
|
1039
|
+
{
|
|
1040
|
+
EmitOff(e.Message);
|
|
1041
|
+
}
|
|
1042
|
+
finally
|
|
1043
|
+
{
|
|
1044
|
+
if (h != IntPtr.Zero)
|
|
1045
|
+
{
|
|
1046
|
+
SetLimit(h, 0);
|
|
1047
|
+
SetPortLink(h, IntPtr.Zero, IntPtr.Zero);
|
|
1048
|
+
NativeMethods.CloseHandle(h);
|
|
1049
|
+
}
|
|
1050
|
+
if (port != IntPtr.Zero)
|
|
1051
|
+
{
|
|
1052
|
+
NativeMethods.CloseHandle(port);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
private static bool SetPortLink(IntPtr h, IntPtr completionKey, IntPtr completionPort)
|
|
1058
|
+
{
|
|
1059
|
+
CompletionPortLink link = new CompletionPortLink();
|
|
1060
|
+
link.CompletionKey = completionKey;
|
|
1061
|
+
link.CompletionPort = completionPort;
|
|
1062
|
+
return NativeMethods.SetInformationJobObject(h, JobObjectAssociateCompletionPortInformation, ref link, Marshal.SizeOf(typeof(CompletionPortLink)));
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
/// <summary>The notification mark in MB of job commit (0 removes it).</summary>
|
|
1066
|
+
private static bool SetLimit(IntPtr h, long mb)
|
|
1067
|
+
{
|
|
1068
|
+
NotificationLimit limit = new NotificationLimit();
|
|
1069
|
+
limit.LimitFlags = mb > 0 ? JobObjectLimitJobMemory : 0;
|
|
1070
|
+
limit.JobMemoryLimit = mb > 0 ? (ulong)(mb * BytesPerMb) : 0;
|
|
1071
|
+
bool ok = NativeMethods.SetInformationJobObject(h, JobObjectNotificationLimitInformation, ref limit, Marshal.SizeOf(typeof(NotificationLimit)));
|
|
1072
|
+
if (ok)
|
|
1073
|
+
{
|
|
1074
|
+
armedMb = mb;
|
|
1075
|
+
}
|
|
1076
|
+
return ok;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
/// <summary>What the job's processes (not this one) hold in private memory, in MB.</summary>
|
|
1080
|
+
private static long JobCommitMb()
|
|
1081
|
+
{
|
|
1082
|
+
ulong total = 0;
|
|
1083
|
+
int self = SelfId;
|
|
1084
|
+
foreach (int pid in JobProcessIds())
|
|
1085
|
+
{
|
|
1086
|
+
if (pid == self)
|
|
1087
|
+
{
|
|
1088
|
+
continue;
|
|
1089
|
+
}
|
|
1090
|
+
IntPtr process = NativeMethods.OpenProcess(ProcessQueryLimited, false, pid);
|
|
1091
|
+
if (process == IntPtr.Zero)
|
|
1092
|
+
{
|
|
1093
|
+
continue;
|
|
1094
|
+
}
|
|
1095
|
+
try
|
|
1096
|
+
{
|
|
1097
|
+
MemoryCounters counters = new MemoryCounters();
|
|
1098
|
+
counters.Size = (uint)Marshal.SizeOf(typeof(MemoryCounters));
|
|
1099
|
+
if (NativeMethods.GetProcessMemoryInfo(process, ref counters, counters.Size))
|
|
1100
|
+
{
|
|
1101
|
+
total += counters.PrivateUsage.ToUInt64();
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
finally
|
|
1105
|
+
{
|
|
1106
|
+
NativeMethods.CloseHandle(process);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
return (long)(total / (ulong)BytesPerMb);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
/// <summary>The lower-case program name (no folder, no .exe) of an open process, or null.</summary>
|
|
1113
|
+
private static string ImageName(IntPtr process)
|
|
1114
|
+
{
|
|
1115
|
+
StringBuilder path = new StringBuilder(1024);
|
|
1116
|
+
uint size = (uint)path.Capacity;
|
|
1117
|
+
if (!NativeMethods.QueryFullProcessImageNameW(process, 0, path, ref size))
|
|
1118
|
+
{
|
|
1119
|
+
return null;
|
|
1120
|
+
}
|
|
1121
|
+
return Path.GetFileNameWithoutExtension(path.ToString(0, (int)size)).ToLowerInvariant();
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
/// <summary>
|
|
1125
|
+
/// Starts watching a process if the gate may freeze it by name; returns its name, or null when it is not one of
|
|
1126
|
+
/// those (or is already watched or held).
|
|
1127
|
+
/// </summary>
|
|
1128
|
+
private static string Track(int pid)
|
|
1129
|
+
{
|
|
1130
|
+
if (pid <= 0 || pid == SelfId)
|
|
1131
|
+
{
|
|
1132
|
+
return null;
|
|
1133
|
+
}
|
|
1134
|
+
IntPtr process = NativeMethods.OpenProcess(ProcessQueryLimited | ProcessSuspendResume, false, pid);
|
|
1135
|
+
if (process == IntPtr.Zero)
|
|
1136
|
+
{
|
|
1137
|
+
return null;
|
|
1138
|
+
}
|
|
1139
|
+
bool kept = false;
|
|
1140
|
+
try
|
|
1141
|
+
{
|
|
1142
|
+
string name = ImageName(process);
|
|
1143
|
+
if (name == null || !gate.Names.Contains(name))
|
|
1144
|
+
{
|
|
1145
|
+
return null;
|
|
1146
|
+
}
|
|
1147
|
+
lock (Gate)
|
|
1148
|
+
{
|
|
1149
|
+
if (Young.ContainsKey(pid) || GateHeld.ContainsKey(pid))
|
|
1150
|
+
{
|
|
1151
|
+
return null;
|
|
1152
|
+
}
|
|
1153
|
+
Tracked t = new Tracked();
|
|
1154
|
+
t.Handle = process;
|
|
1155
|
+
t.Name = name;
|
|
1156
|
+
t.Started = DateTime.UtcNow;
|
|
1157
|
+
Young[pid] = t;
|
|
1158
|
+
}
|
|
1159
|
+
kept = true;
|
|
1160
|
+
return name;
|
|
1161
|
+
}
|
|
1162
|
+
finally
|
|
1163
|
+
{
|
|
1164
|
+
if (!kept)
|
|
1165
|
+
{
|
|
1166
|
+
NativeMethods.CloseHandle(process);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
private static void OnBirth(int pid)
|
|
1172
|
+
{
|
|
1173
|
+
string name = Track(pid);
|
|
1174
|
+
if (name != null && BurstActive(DateTime.UtcNow) && Hold(pid))
|
|
1175
|
+
{
|
|
1176
|
+
Emit(EventStart("hold") + ",\"pid\":" + Num(pid) + ",\"name\":" + Json(name) + "}");
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
private static void OnExit(int pid)
|
|
1181
|
+
{
|
|
1182
|
+
lock (Gate)
|
|
1183
|
+
{
|
|
1184
|
+
Tracked t;
|
|
1185
|
+
if (Young.TryGetValue(pid, out t))
|
|
1186
|
+
{
|
|
1187
|
+
NativeMethods.CloseHandle(t.Handle);
|
|
1188
|
+
Young.Remove(pid);
|
|
1189
|
+
}
|
|
1190
|
+
if (GateHeld.TryGetValue(pid, out t))
|
|
1191
|
+
{
|
|
1192
|
+
NativeMethods.CloseHandle(t.Handle);
|
|
1193
|
+
GateHeld.Remove(pid);
|
|
1194
|
+
SaveHeld();
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
/// <summary>The job crossed its mark. The query re-arms the notification, so it comes first and always.</summary>
|
|
1200
|
+
private static void OnNotified(IntPtr h, int pid)
|
|
1201
|
+
{
|
|
1202
|
+
LimitViolation v = new LimitViolation();
|
|
1203
|
+
bool queried = NativeMethods.QueryInformationJobObject(h, JobObjectLimitViolationInformation, ref v, Marshal.SizeOf(typeof(LimitViolation)), IntPtr.Zero);
|
|
1204
|
+
long jobMb;
|
|
1205
|
+
if (queried && (v.ViolationLimitFlags & JobObjectLimitJobMemory) != 0)
|
|
1206
|
+
{
|
|
1207
|
+
jobMb = (long)(v.JobMemory / (ulong)BytesPerMb);
|
|
1208
|
+
}
|
|
1209
|
+
else
|
|
1210
|
+
{
|
|
1211
|
+
jobMb = JobCommitMb();
|
|
1212
|
+
}
|
|
1213
|
+
Trip(h, true, pid, jobMb);
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
/// <summary>The lowest job commit in the window and when it was.</summary>
|
|
1217
|
+
private static long WindowMin(out DateTime at)
|
|
1218
|
+
{
|
|
1219
|
+
long min = long.MaxValue;
|
|
1220
|
+
at = DateTime.UtcNow;
|
|
1221
|
+
foreach (KeyValuePair<DateTime, long> entry in window)
|
|
1222
|
+
{
|
|
1223
|
+
if (entry.Value < min)
|
|
1224
|
+
{
|
|
1225
|
+
min = entry.Value;
|
|
1226
|
+
at = entry.Key;
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
return min;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
/// <summary>Every GateTickMs: the window, young entries, births in poll mode, the software trip and the moving mark.</summary>
|
|
1233
|
+
private static void Tick(IntPtr h, bool notify)
|
|
1234
|
+
{
|
|
1235
|
+
DateTime now = DateTime.UtcNow;
|
|
1236
|
+
long jobMb = JobCommitMb();
|
|
1237
|
+
window.Add(new KeyValuePair<DateTime, long>(now, jobMb));
|
|
1238
|
+
window.RemoveAll(delegate(KeyValuePair<DateTime, long> e) { return (now - e.Key).TotalMilliseconds > gate.WindowMs; });
|
|
1239
|
+
DateTime minAt;
|
|
1240
|
+
long min = WindowMin(out minAt);
|
|
1241
|
+
lock (Gate)
|
|
1242
|
+
{
|
|
1243
|
+
List<int> old = new List<int>();
|
|
1244
|
+
foreach (KeyValuePair<int, Tracked> p in Young)
|
|
1245
|
+
{
|
|
1246
|
+
if ((now - p.Value.Started).TotalMilliseconds > gate.WindowMs)
|
|
1247
|
+
{
|
|
1248
|
+
old.Add(p.Key);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
foreach (int pid in old)
|
|
1252
|
+
{
|
|
1253
|
+
NativeMethods.CloseHandle(Young[pid].Handle);
|
|
1254
|
+
Young.Remove(pid);
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
if (!notify)
|
|
1258
|
+
{
|
|
1259
|
+
List<int> current = JobProcessIds();
|
|
1260
|
+
foreach (int pid in current)
|
|
1261
|
+
{
|
|
1262
|
+
if (!seenPids.Contains(pid))
|
|
1263
|
+
{
|
|
1264
|
+
OnBirth(pid);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
seenPids = new HashSet<int>(current);
|
|
1268
|
+
}
|
|
1269
|
+
if (jobMb - min >= gate.GrowthMb)
|
|
1270
|
+
{
|
|
1271
|
+
Trip(h, notify, 0, jobMb);
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
if (notify)
|
|
1275
|
+
{
|
|
1276
|
+
long target = min + gate.GrowthMb;
|
|
1277
|
+
if (Math.Abs(target - armedMb) >= GateRearmStepMb)
|
|
1278
|
+
{
|
|
1279
|
+
SetLimit(h, target);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
/// <summary>A burst: freeze the young build programs (and the one that caused it), then move the mark up.</summary>
|
|
1285
|
+
private static void Trip(IntPtr h, bool notify, int causePid, long jobMb)
|
|
1286
|
+
{
|
|
1287
|
+
DateTime now = DateTime.UtcNow;
|
|
1288
|
+
long grew = 0;
|
|
1289
|
+
long ms = 0;
|
|
1290
|
+
if (window.Count > 0)
|
|
1291
|
+
{
|
|
1292
|
+
DateTime minAt;
|
|
1293
|
+
long min = WindowMin(out minAt);
|
|
1294
|
+
grew = Math.Max(0, jobMb - min);
|
|
1295
|
+
ms = Math.Max(0, (long)(now - minAt).TotalMilliseconds);
|
|
1296
|
+
}
|
|
1297
|
+
lastTrip = now;
|
|
1298
|
+
if (causePid > 0)
|
|
1299
|
+
{
|
|
1300
|
+
// The program that caused the notification, even if it is old (a no-op unless the gate may freeze its name).
|
|
1301
|
+
Track(causePid);
|
|
1302
|
+
}
|
|
1303
|
+
List<KeyValuePair<int, Tracked>> victims = new List<KeyValuePair<int, Tracked>>();
|
|
1304
|
+
lock (Gate)
|
|
1305
|
+
{
|
|
1306
|
+
foreach (KeyValuePair<int, Tracked> p in Young)
|
|
1307
|
+
{
|
|
1308
|
+
if (p.Key == causePid || (now - p.Value.Started).TotalMilliseconds < gate.WindowMs)
|
|
1309
|
+
{
|
|
1310
|
+
victims.Add(p);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
victims.Sort(delegate(KeyValuePair<int, Tracked> a, KeyValuePair<int, Tracked> b) { return a.Value.Started.CompareTo(b.Value.Started); });
|
|
1315
|
+
StringBuilder held = new StringBuilder();
|
|
1316
|
+
int count = 0;
|
|
1317
|
+
foreach (KeyValuePair<int, Tracked> victim in victims)
|
|
1318
|
+
{
|
|
1319
|
+
string name = victim.Value.Name;
|
|
1320
|
+
if (!Hold(victim.Key) || count >= MaxTripList)
|
|
1321
|
+
{
|
|
1322
|
+
continue;
|
|
1323
|
+
}
|
|
1324
|
+
if (count > 0)
|
|
1325
|
+
{
|
|
1326
|
+
held.Append(',');
|
|
1327
|
+
}
|
|
1328
|
+
held.Append("{\"pid\":").Append(Num(victim.Key)).Append(",\"name\":").Append(Json(name)).Append('}');
|
|
1329
|
+
count++;
|
|
1330
|
+
}
|
|
1331
|
+
Emit(EventStart("trip") + ",\"grewMb\":" + Num(grew) + ",\"ms\":" + Num(ms) + ",\"jobMb\":" + Num(jobMb)
|
|
1332
|
+
+ ",\"held\":[" + held.ToString() + "]}");
|
|
1333
|
+
window = new List<KeyValuePair<DateTime, long>>();
|
|
1334
|
+
window.Add(new KeyValuePair<DateTime, long>(now, jobMb));
|
|
1335
|
+
if (notify)
|
|
1336
|
+
{
|
|
1337
|
+
SetLimit(h, jobMb + gate.GrowthMb);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
/// <summary>
|
|
1342
|
+
/// A burst went through less than WindowMs ago. Not "something is held": with fast compiles the queue would never
|
|
1343
|
+
/// empty, and every later program would start one per ReleaseMs while memory is calm.
|
|
1344
|
+
/// </summary>
|
|
1345
|
+
private static bool BurstActive(DateTime now)
|
|
1346
|
+
{
|
|
1347
|
+
lock (Gate)
|
|
1348
|
+
{
|
|
1349
|
+
return (now - lastTrip).TotalMilliseconds < gate.WindowMs;
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
/// <summary>Freezes a watched process (unless the governor's timed pause has it); false when it was not frozen.</summary>
|
|
1354
|
+
private static bool Hold(int pid)
|
|
1355
|
+
{
|
|
1356
|
+
lock (Gate)
|
|
1357
|
+
{
|
|
1358
|
+
Tracked t;
|
|
1359
|
+
if (Paused.ContainsKey(pid) || GateHeld.ContainsKey(pid) || !Young.TryGetValue(pid, out t))
|
|
1360
|
+
{
|
|
1361
|
+
return false;
|
|
1362
|
+
}
|
|
1363
|
+
Young.Remove(pid);
|
|
1364
|
+
t.HeldAt = DateTime.UtcNow;
|
|
1365
|
+
GateHeld[pid] = t;
|
|
1366
|
+
// Recorded before the freeze, like the governor's pauses: a memgov that dies right after is undone by the next.
|
|
1367
|
+
SaveHeld();
|
|
1368
|
+
int status = NativeMethods.NtSuspendProcess(t.Handle);
|
|
1369
|
+
if (status != 0)
|
|
1370
|
+
{
|
|
1371
|
+
GateHeld.Remove(pid);
|
|
1372
|
+
NativeMethods.CloseHandle(t.Handle);
|
|
1373
|
+
SaveHeld();
|
|
1374
|
+
return false;
|
|
1375
|
+
}
|
|
1376
|
+
return true;
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
/// <summary>Lets go what is due: anything held MaxHoldMs, and one process (the oldest) every ReleaseMs after a calm moment.</summary>
|
|
1381
|
+
private static void ReleaseDue()
|
|
1382
|
+
{
|
|
1383
|
+
List<Released> done = new List<Released>();
|
|
1384
|
+
lock (Gate)
|
|
1385
|
+
{
|
|
1386
|
+
if (GateHeld.Count == 0)
|
|
1387
|
+
{
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1390
|
+
DateTime now = DateTime.UtcNow;
|
|
1391
|
+
List<int> forced = new List<int>();
|
|
1392
|
+
foreach (KeyValuePair<int, Tracked> p in GateHeld)
|
|
1393
|
+
{
|
|
1394
|
+
if ((now - p.Value.HeldAt).TotalMilliseconds >= gate.MaxHoldMs)
|
|
1395
|
+
{
|
|
1396
|
+
forced.Add(p.Key);
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
foreach (int pid in forced)
|
|
1400
|
+
{
|
|
1401
|
+
done.Add(ReleaseOne(pid, now, true));
|
|
1402
|
+
}
|
|
1403
|
+
if (GateHeld.Count > 0 && (now - lastTrip).TotalMilliseconds >= gate.ReleaseMs
|
|
1404
|
+
&& (now - lastRelease).TotalMilliseconds >= gate.ReleaseMs)
|
|
1405
|
+
{
|
|
1406
|
+
int oldest = 0;
|
|
1407
|
+
DateTime oldestAt = DateTime.MaxValue;
|
|
1408
|
+
foreach (KeyValuePair<int, Tracked> p in GateHeld)
|
|
1409
|
+
{
|
|
1410
|
+
if (p.Value.HeldAt < oldestAt || (p.Value.HeldAt == oldestAt && p.Key < oldest))
|
|
1411
|
+
{
|
|
1412
|
+
oldest = p.Key;
|
|
1413
|
+
oldestAt = p.Value.HeldAt;
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
done.Add(ReleaseOne(oldest, now, false));
|
|
1417
|
+
lastRelease = now;
|
|
1418
|
+
}
|
|
1419
|
+
if (done.Count > 0)
|
|
1420
|
+
{
|
|
1421
|
+
SaveHeld();
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
foreach (Released r in done)
|
|
1425
|
+
{
|
|
1426
|
+
Emit(EventStart("release") + ",\"pid\":" + Num(r.Pid) + ",\"name\":" + Json(r.Name) + ",\"heldMs\":" + Num(r.HeldMs)
|
|
1427
|
+
+ ",\"forced\":" + (r.Forced ? "true" : "false") + "}");
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
/// <summary>Resumes and forgets one held process. Called with Gate held.</summary>
|
|
1432
|
+
private static Released ReleaseOne(int pid, DateTime now, bool forced)
|
|
1433
|
+
{
|
|
1434
|
+
Tracked t = GateHeld[pid];
|
|
1435
|
+
NativeMethods.NtResumeProcess(t.Handle);
|
|
1436
|
+
NativeMethods.CloseHandle(t.Handle);
|
|
1437
|
+
GateHeld.Remove(pid);
|
|
1438
|
+
Released r = new Released();
|
|
1439
|
+
r.Pid = pid;
|
|
1440
|
+
r.Name = t.Name;
|
|
1441
|
+
r.HeldMs = (long)(now - t.HeldAt).TotalMilliseconds;
|
|
1442
|
+
r.Forced = forced;
|
|
1443
|
+
return r;
|
|
1444
|
+
}
|
|
1445
|
+
|
|
701
1446
|
private static string Mb(ulong bytes)
|
|
702
1447
|
{
|
|
703
1448
|
return Math.Floor(bytes / BytesPerMb).ToString("0", CultureInfo.InvariantCulture);
|