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. 9 import Base 10 import 0xabe575924687afad4cee1a2c1194d639/main.bend as R 11 import ./env.bend as Env 12 import ./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. 23 def 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. 30 def ok() -> IO(Bool): 31 do IO: 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. 38 def 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 43 def 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. 53 def 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 57 def run(on: Bool, args: List<&2, String>) -> IO(String): 58 do IO: 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 64 def 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 68 def 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 73 def 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 77 def 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 85 def 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())