@codesema/runsema 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +133 -2
- package/bin/runsema-darwin-arm64 +0 -0
- package/bin/runsema-darwin-x64 +0 -0
- package/bin/runsema-linux-arm64 +0 -0
- package/bin/runsema-linux-x64 +0 -0
- package/bin/runsema.js +31 -0
- package/package.json +23 -5
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hasan TASKIN
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,134 @@
|
|
|
1
|
-
#
|
|
1
|
+
# runsema
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A small launcher that runs a command with the share of the machine that is free right now, CPU and
|
|
4
|
+
memory, instead of a fixed worker count.
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
runsema -- vitest run --maxWorkers={jobs}
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Linux and macOS. Go, standard library only, one static binary.
|
|
11
|
+
|
|
12
|
+
## Why
|
|
13
|
+
|
|
14
|
+
Test runners, bundlers and linters size their workers once, from the core count. Two of them started
|
|
15
|
+
together each believe they own the machine, and so do the tools of every other terminal, agent session
|
|
16
|
+
or worktree. The usual answer is a fixed cap (`--maxWorkers=4`, `--concurrency=2`): too low when the
|
|
17
|
+
machine is idle, too high when it is busy. And a cap on workers says nothing about memory, which is
|
|
18
|
+
what kills processes.
|
|
19
|
+
|
|
20
|
+
runsema makes the cap follow the machine. Every run asks before it starts, gets what is free, and gives
|
|
21
|
+
it back when it ends. It learns what each command uses, so nothing has to be tuned. There is no daemon
|
|
22
|
+
and no configuration.
|
|
23
|
+
|
|
24
|
+
## How it works
|
|
25
|
+
|
|
26
|
+
1. **Slots.** One lock file per CPU, in a directory shared by every run. runsema takes the free ones
|
|
27
|
+
with `flock`, up to `-max`. The kernel frees them when runsema exits, whatever the reason, so a
|
|
28
|
+
killed run never leaks its slots.
|
|
29
|
+
2. **Learning.** While a command runs, runsema measures the memory of its whole process tree. When it
|
|
30
|
+
ends, the peak is stored with the number of jobs it had. The weight of a command is the heaviest of
|
|
31
|
+
its last 10 runs, per job: a suite that grows counts from its next run.
|
|
32
|
+
3. **Memory.** A command already measured gets as many jobs as fit in the memory available right now,
|
|
33
|
+
keeping 10% of the machine free. It announces what it is about to use, so the runs admitted behind
|
|
34
|
+
it do not count the same free memory twice while it loads.
|
|
35
|
+
4. **Gate.** Runs are admitted one at a time. A command never measured keeps the gate closed until its
|
|
36
|
+
memory stops growing (30 s at most): it loads alone, then the next run decides on real figures.
|
|
37
|
+
5. **Margin.** Every weight is multiplied by a safety margin, 1.2 at first and never under 1.1. A run
|
|
38
|
+
during which the machine ran short of memory widens it by a quarter; a quiet run tightens it a
|
|
39
|
+
little. The margin tunes itself.
|
|
40
|
+
6. **Pressure.** runsema reads Linux pressure stall information (PSI): the share of the last 10 s some
|
|
41
|
+
task spent waiting for CPU or memory. Above `-pressure` it waits, because something that does not
|
|
42
|
+
go through runsema is already loading the machine.
|
|
43
|
+
7. **Grant.** The command starts with the jobs that passed all of the above. After `-timeout` it starts
|
|
44
|
+
anyway with `-min`: runsema delays a run, it never blocks one.
|
|
45
|
+
8. **Hand-over.** The grant reaches the command as `{jobs}` replaced in its arguments and as the
|
|
46
|
+
`RUNSEMA_JOBS` environment variable. Use whichever the tool can read.
|
|
47
|
+
9. **Nesting.** A runsema started under another one reuses the grant of its parent and reserves
|
|
48
|
+
nothing, so a task runner and the tools it starts can both be wrapped.
|
|
49
|
+
|
|
50
|
+
The command gets the terminal as is, receives the signals runsema receives, and its exit code is the
|
|
51
|
+
exit code of runsema.
|
|
52
|
+
|
|
53
|
+
Inside a container the CPU count is the cgroup quota (`cpu.max`), the memory is the cgroup limit
|
|
54
|
+
(`memory.max`) and the pressure is the one of the container itself, so a busy neighbour does not slow
|
|
55
|
+
the run down. On a host all three are machine-wide.
|
|
56
|
+
|
|
57
|
+
## Install
|
|
58
|
+
|
|
59
|
+
With npm, pnpm, yarn or bun. One package holds the binary of every platform and starts the one that
|
|
60
|
+
matches the machine, so nothing is downloaded at install and there is no optional dependency to resolve:
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
npm install --save-dev @codesema/runsema
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
With Go:
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
go install github.com/getCodesema/runsema@latest
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Or build it and copy the binary anywhere:
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o runsema .
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Usage
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
runsema -- jest --maxWorkers={jobs}
|
|
82
|
+
runsema -- turbo run test --concurrency={jobs}
|
|
83
|
+
runsema -max 4 -- playwright test --workers={jobs}
|
|
84
|
+
runsema -- sh -c 'make -j"$RUNSEMA_JOBS"'
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
| Flag | Default | Meaning |
|
|
88
|
+
| ----------- | ----------------- | ---------------------------------------------------------------- |
|
|
89
|
+
| `-min` | `1` | Smallest grant; the run waits for it until `-timeout` |
|
|
90
|
+
| `-max` | half the CPUs | Largest grant, so that two full runs fit side by side |
|
|
91
|
+
| `-pressure` | `25` | PSI `avg10` percentage above which the run waits |
|
|
92
|
+
| `-timeout` | `5m` | Longest wait before running anyway with `-min` |
|
|
93
|
+
| `-io` | off | Also watch IO pressure (CPU and memory are always watched) |
|
|
94
|
+
| `-quiet` | off | Do not print the grant on stderr |
|
|
95
|
+
| `-dir` | see below | Directory of the lock files (slots, gate, reservations) |
|
|
96
|
+
| `-state` | see below | File remembering what each command used |
|
|
97
|
+
|
|
98
|
+
The lock directory is `$RUNSEMA_DIR`, else `$XDG_RUNTIME_DIR/runsema`, else `/tmp/runsema-<uid>`. Runs
|
|
99
|
+
share the machine only when they share this directory. What runsema learned is kept in
|
|
100
|
+
`runsema/profiles.json` under the user cache directory (`~/.cache` on Linux); delete it to forget.
|
|
101
|
+
|
|
102
|
+
A command is recognised by its directory and its arguments: the same script in another worktree is
|
|
103
|
+
measured again.
|
|
104
|
+
|
|
105
|
+
## Limits
|
|
106
|
+
|
|
107
|
+
- The grant is fixed when the command starts: a run does not grow when the machine frees up, and a
|
|
108
|
+
run that outgrows what it announced is not stopped.
|
|
109
|
+
- Memory is counted per job, as if it grew in proportion: a command with a large fixed part is
|
|
110
|
+
overestimated at low job counts.
|
|
111
|
+
- A command that ends in under a second is never measured.
|
|
112
|
+
- Nested runs each reuse the whole grant of their parent; it is not divided between siblings.
|
|
113
|
+
- A tool that ignores both `{jobs}` and `RUNSEMA_JOBS` is only delayed, not sized.
|
|
114
|
+
- Without PSI (kernel before 4.20, or PSI disabled) the slots and the memory still apply.
|
|
115
|
+
- macOS has neither PSI nor `/proc`: only the slots apply there. No Windows (`flock`).
|
|
116
|
+
|
|
117
|
+
## Development
|
|
118
|
+
|
|
119
|
+
```sh
|
|
120
|
+
go vet ./... && go test ./...
|
|
121
|
+
sh scripts/smoke-npm.sh
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`admission.go` holds the decisions as pure functions (`bytesPerJob`, `jobsThatFit`, `rampStable`,
|
|
125
|
+
`nextMargin`). `memory.go` reads the machine: memory, process trees, PSI. `profile.go` stores what was
|
|
126
|
+
learned. `main.go` is the flow: gate, slots, reservations, the command, the measure. Each has its test
|
|
127
|
+
file, on temporary directories.
|
|
128
|
+
|
|
129
|
+
`scripts/build-npm.sh <version>` builds the npm package into `dist/npm/runsema`: the launcher and one
|
|
130
|
+
binary per platform. `scripts/smoke-npm.sh` installs it in an empty project and runs the command.
|
|
131
|
+
|
|
132
|
+
## License
|
|
133
|
+
|
|
134
|
+
[MIT](LICENSE)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/bin/runsema.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// Starts the binary built for this machine, shipped next to this file, and gets out of the way:
|
|
5
|
+
// same arguments, same terminal, same signals, same exit code.
|
|
6
|
+
const { spawn } = require('node:child_process');
|
|
7
|
+
const { existsSync } = require('node:fs');
|
|
8
|
+
const { constants } = require('node:os');
|
|
9
|
+
const { join } = require('node:path');
|
|
10
|
+
|
|
11
|
+
const binary = join(__dirname, `runsema-${process.platform}-${process.arch}`);
|
|
12
|
+
|
|
13
|
+
if (!existsSync(binary)) {
|
|
14
|
+
console.error(`runsema: no binary for ${process.platform} ${process.arch} (runsema runs on Linux and macOS, x64 or arm64).`);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const child = spawn(binary, process.argv.slice(2), { stdio: 'inherit' });
|
|
19
|
+
|
|
20
|
+
for (const name of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
|
|
21
|
+
process.on(name, () => child.kill(name));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
child.on('error', (error) => {
|
|
25
|
+
console.error(`runsema: ${error.message}`);
|
|
26
|
+
process.exit(127);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
child.on('exit', (code, signal) => {
|
|
30
|
+
process.exit(signal === null ? code : 128 + constants.signals[signal]);
|
|
31
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "@codesema/runsema",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Launcher that sizes a command's parallelism from the CPUs and memory free right now: shared flock slots, learned memory, Linux pressure (PSI).",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/getCodesema/runsema.git"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"runsema": "bin/runsema.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"bin"
|
|
15
|
+
],
|
|
16
|
+
"os": [
|
|
17
|
+
"darwin",
|
|
18
|
+
"linux"
|
|
19
|
+
],
|
|
20
|
+
"cpu": [
|
|
21
|
+
"arm64",
|
|
22
|
+
"x64"
|
|
23
|
+
]
|
|
24
|
+
}
|