src/share/cap.bend

ezx@1.11.0.0 · 90 lines · 11 definitions

1# share/cap: every `bend` the runner starts, inside a memory cap.
2#
3# bend's C backend, not clang, is what costs the memory: emitting the C for a
4# large program peaked at 18.8 GB here while clang on that same C peaked at
5# 242 MB. An uncapped run reached 35 GB and took the host down. Worse, bend
6# prints that all terms check *before* it emits any C, so a build the kernel
7# kills for memory looks exactly like one that succeeded and happened to write
8# no binary. The cap is what turns that into a legible exit 137.
9import Base
10import snap@1.4.0.0/main.bend as R
11import ezx@1.11.0.0/src/share/env.bend as Env
12import ezx@1.11.0.0/src/share/args.bend as Args
13
14# how many gigabytes one `bend` may have. 8 is what fits beside the rest of
15# this machine; a project whose closure costs more says so in EZ_CAP.
16#
17# The cap is also the divisor the gate picks its width from, since a run has to
18# assume every job it starts at once could want the whole of it. So a cap set
19# higher than a tree needs does not only reserve memory, it narrows the gate:
20# the dearest aggregate here peaks at 3.3G, and at the 8G cap that is a width
21# of 4 where 11 would fit. Lower EZ_CAP, or set EZ_JOBS outright, for a tree
22# you have measured.
23def gb() -> IO(String):
24  Env.var("EZ_CAP", "8")
25
26# whether a `bend` can be capped. The cap is `systemd-run --user`. That
27# fails when the program is absent, and it fails when the program is present
28# and the user bus is not. Either failure is no cap. A probe that would
29# wait on a password fails closed instead.
30def ok() -> IO(Bool):
31  do IO<Bool>:
32    +out : String <- R.exec(["systemd-run", "--user", "--scope", "-q",
33      "--collect", "--no-ask-password", "true"])
34    return R.ok(out)
35
36# the cgroup a capped run gets. `MemorySwapMax=0` matters: without it the
37# cgroup spills into swap and the cap stops meaning anything.
38def scope(+gigs: String) -> List<&2, String>:
39  ["systemd-run", "--user", "--scope", "-q", "-p", "MemoryMax=" ++ gigs ++ "G",
40    "-p", "MemorySwapMax=0", "nice"]
41
42# the words a capped run is prefixed with, or none when nothing can cap it
43def pre(on: Bool, +gigs: String) -> List<&2, String>:
44  match on:
45    case True{}:
46      scope(gigs)
47    case False{}:
48      []
49
50# a program's arguments dressed the way the cap wants them, for a caller that
51# is not going to run it here. The cap goes outside `env`, so the variable is
52# set inside the cgroup.
53def argv(on: Bool, +gigs: String, +at: String, args: List<&2, String>) -> List<&2, String>:
54  List.append(&2, String, pre(on, gigs), Env.line(at, args))
55
56# a program run under the cap, with this project's BEND_LIB set for it
57def run(on: Bool, args: List<&2, String>) -> IO(String):
58  do IO<String>:
59    +g : String <- gb()
60    at : String <- Env.lib()
61    R.exec(argv(on, g, at, args))
62
63# the status the kernel leaves on a process it killed for memory
64def killed(+out: String) -> Bool:
65  String.eq(R.code(out), "137")
66
67# what to say when a run died for memory rather than for being wrong
68def why(+out: String, +gigs: String) -> String:
69  Bool.pick(String, killed(out),
70    "killed for memory at " ++ gigs ++ "G: raise EZ_CAP\n", "")
71
72# what is said when nothing is capping the jobs
73def warning() -> String:
74  "warning: systemd-run --user cannot cap `bend`, so no `bend` is capped"
75
76# a warning printed once when nothing is capping the jobs
77def warn(on: Bool) -> IO(Unit):
78  match on:
79    case True{}:
80      IO.pure(Unit, Unit{})
81    case False{}:
82      IO.write(warning() ++ "\n")
83
84# the same warning on stderr, for a command whose stdout is the program's
85def warn.err(on: Bool) -> IO(Unit):
86  match on:
87    case True{}:
88      IO.pure(Unit, Unit{})
89    case False{}:
90      IO.print_err(warning())