How Much Memory Does a CI Build Need? Five Real Builds at 2, 4 and 8 GB
---
title: How Much Memory Does a CI Build Need? Five Real Builds at 2, 4 and 8 GB
published: true
date: 2026-11-02 18:00:00 UTC
tags: CICD,Memory,cgroups,Docker
canonical_url: devops-daily.com
---
We ran five real open-source builds (a Vite app build, a TypeScript type check, a Gradle build, a Go build and a Docker image build) inside 2, 4 and 8 GB memory limits on one 4 vCPU machine, with Gradle both with and without its daemon, two or three times at each limit. Four of the five finished inside 2 GB. The one that did not was the Excalidraw web app: its Vite production build also runs a full TypeScript check, together they held 3,248 MiB of anonymous memory, and the kernel killed the build at 2 GB in all 3 runs.
The number that most tools show you was the wrong one for the other four. cgroup v2's memory.peak said the GitHub CLI's Go build peaked at 1,775 MiB, but only 424 MiB of that was anonymous memory, the heaps and stacks of its processes. Most of the rest was page cache, which the kernel drops when it needs the room, plus some kernel memory. A Docker image build looked like 2,072 MiB for the same reason. And a TypeScript type check that held 1,139 MiB with the whole machine available finished inside 2 GB with 970 MiB, because Node sizes its heap limit from the cgroup limit it finds.
This post shows the numbers, the five traps behind them, and every script we used, so you can size a runner from your own builds instead of from a guess.
TL;DR
- Four of five builds fit in 2 GB. Only the Excalidraw Vite build needed more: 3,248 MiB of anonymous memory, killed at 2 GB, fine at 4 GB.
- memory.peak counts page cache. The Go build and the Docker build showed 1,775 and 2,072 MiB, but their anonymous memory peaked at 424 and 355 MiB. Size from anonymous memory plus a margin, then confirm by running the build inside that limit.
- Runtimes size their heaps from the cgroup limit. Inside 2 GB, Node's default heap limit was 1,120 MiB and the JVM's default maximum heap was 512 MiB. The type check used less memory at 2 GB than at 8 GB and still finished, about 24% slower.
- The Gradle daemon keeps its memory after the build. Five seconds after gradlew exited, the daemon still held 1,048 MiB of anonymous memory. With --no-daemon, that dropped to under 1 MiB, and in our one-command runs the daemon gave no repeatable speed-up.
- Limiting the docker CLI did not limit the build. The CLI's cgroup peaked at 40 MiB while the BuildKit container that did the work peaked at 2,072 MiB.
- Wall times on a shared-CPU droplet are noisy: the CPUs reported 23.5% steal time across the runs we tracked. Trust the memory columns more than the time columns.
Prerequisites
- A Linux machine with cgroup v2 and systemd (any recent Ubuntu, Debian or Fedora). Check with stat -fc %T /sys/fs/cgroup, which prints cgroup2fs.
- Root access, for systemd-run --scope and for dropping the page cache.
- The toolchains for the builds you want to test. Our setup script installs Go, Node, Yarn, a JDK, Rust and Docker.
- If you want to rerun our matrix as it is: a 4 vCPU, 8 GB machine and about two hours. We used a droplet on DigitalOcean (referral link).
How we measured
We used one DigitalOcean droplet, size s-4vcpu-8gb in fra1, with Ubuntu 24.04 and kernel 6.8. The kernel reports 7,941 MiB of memory and there is no swap. Every build ran in its own transient systemd scope:
systemd-run --scope --slice=bench.slice --unit=p1-<build>-<limit>-<run> \
-p MemoryMax=2G -p MemorySwapMax=0 -p AllowedCPUs=0-3 -p OOMPolicy=continue \
/root/bench/wrap.sh <outdir> <timeout> <build command>
These are cgroup limits on one droplet, not three real machine sizes. A real 2 GB runner has a smaller kernel footprint, a runner agent and often a Docker daemon in the same memory, so it has less room than our 2 GB scope. The 8 GB limit is above the 7.9 GiB that the droplet has, so the 8 GB column means "the whole machine".
The rest of the setup:
- CPU. Every scope was pinned to the same 4 vCPUs with AllowedCPUs=0-3, and nproc printed 4 inside each one. Compilers start about one job per CPU, so a 2 vCPU runner would use less memory and an 8 vCPU runner more. We did not measure other CPU counts.
- Cold build, warm dependencies. We downloaded dependencies once before the runs: yarn install, go mod download, cargo fetch, the Gradle distribution and dependencies, and the base images for the Docker build. Before every run we deleted the build output and the compiler caches (excalidraw-app/build, GOCACHE, target/, Gradle's build/ and .gradle/), stopped any Gradle daemon, and dropped the page cache. That is a CI job that restores a dependency cache but not a build cache.
- What we recorded. Wall time, the exit code, memory.peak at the end of the run, memory.events (how often the limit was hit, and OOM kills), and a sampler that read memory.stat every 100 ms for anonymous memory (anon) and page cache (file).
- Repeats. Runs went in round-robin order: run 1 of every cell, then run 2, then run 3, so a slow period on the shared host does not land on one build. Builds ran slower than we planned, so to stay inside our droplet budget the third round covered only the 8 GB and 2 GB limits. That gives 3 runs per cell at 8 GB and 2 GB, and 2 at 4 GB. The tables show the median of 3, or the mean of 2 at 4 GB. Each build also had one warm-up run at 8 GB that we did not count; it downloaded Gradle and filled the image mirror.
The builds, all pinned to exact commits:
| Build | Command | Source |
| --- | --- | --- |
| Excalidraw app, Vite production build | yarn build:app:docker | excalidraw/excalidraw 8bcdca45 |
| Excalidraw type check, 1,508 files | tsc -p tsconfig.json (noEmit is set) | same checkout |
| PlantUML, Gradle 9.3.1 on JDK 21 | ./gradlew jar, daemon on and --no-daemon | plantuml/plantuml db06d70e |
| GitHub CLI, Go 1.27.3 | go build ./cmd/gh | cli/cli 9490d204 |
| Online Boutique frontend image, BuildKit 0.34.1 | docker buildx build | GoogleCloudPlatform/microservices-demo c7cb6d5b |
| ripgrep 15.2.0, Rust 1.99.1 (one run) | cargo build --release --locked | BurntSushi/ripgrep tag 15.2.0 |
Node was 24.22.2 with Yarn 1.22.22, Docker was 29.9.2 with Buildx 0.38.1.
The results
| Build | Anonymous peak at 8 GB | memory.peak at 8 GB | 8 GB | 4 GB | 2 GB |
| --- | --- | --- | --- | --- | --- |
| Excalidraw vite build | 3,248 MiB | 3,578 MiB | ok, 66 s | ok, 83 s | 3 of 3 OOM-killed (run 1 from the kernel log) |
| Excalidraw tsc type check | 1,139 MiB | 1,271 MiB | ok, 59 s | ok, 62 s | ok, 73 s |
| PlantUML Gradle, daemon | 1,143 MiB | 1,474 MiB | ok, 124 s | ok, 110 s | ok, 113 s |
| PlantUML Gradle, --no-daemon | 1,144 MiB | 1,475 MiB | ok, 88 s | ok, 119 s | ok, 100 s |
| GitHub CLI go build | 424 MiB | 1,775 MiB | ok, 141 s | ok, 198 s | ok, 169 s |
| Online Boutique frontend, docker buildx build | 355 MiB | 2,072 MiB | ok, 131 s | ok, 129 s | ok, 110 s |
Times are medians of 3 runs at 8 GB and 2 GB, and the mean of 2 runs at 4 GB. Memory columns are from the 8 GB runs.
What the cgroup counter says vs what the build held (8 GB limit, median of 3)
| | Value | Series |
| --- | --- | --- |
| Vite build | 3578 MiB | memory.peak |
| Vite build | 3248 MiB | anonymous memory |
| tsc check | 1271 MiB | memory.peak |
| tsc check | 1139 MiB | anonymous memory |
| Gradle (daemon) | 1474 MiB | memory.peak |
| Gradle (daemon) | 1143 MiB | anonymous memory |
| Gradle (no daemon) | 1475 MiB | memory.peak |
| Gradle (no daemon) | 1144 MiB | anonymous memory |
| Go build | 1775 MiB | memory.peak |
| Go build | 424 MiB | anonymous memory |
| Docker build | 2072 MiB | memory.peak |
| Docker build | 355 MiB | anonymous memory |
_memory.peak includes page cache; anonymous memory is the sampled peak of anon in memory.stat (100 ms). One s-4vcpu-8gb droplet, 4 vCPUs, 2 November 2026._
Three things stand out. Only one build reached the 2 GB limit at all. The max counter in memory.events, which counts how often a cgroup's usage was about to go over its limit, stayed at 0 for every other run. Memory use at 4 GB and at 8 GB was almost the same for every build, so the extra 4 GB bought nothing. And the gap between memory.peak and anonymous memory is large for exactly the builds that read and write many files: the Go compiler and BuildKit.
The single Rust build, ripgrep 15.2.0 with cargo build --release, fits the same pattern: 896 MiB memory.peak, 572 MiB anonymous, 61.1 s. We ran it once at 8 GB only, because the full matrix did not fit our time budget.
Trap 1: memory.peak counts page cache
memory.peak is the highest value of memory.current that the cgroup reached. memory.current charges the cgroup for everything it caused to be in memory, and that includes page cache : the source files the compiler read, the object files it wrote, the layers BuildKit unpacked. Page cache is reclaimable. When the cgroup gets close to its limit, the kernel drops clean cache pages and writes back dirty ones before it ever considers killing a process.
Anonymous memory is the part that cannot go anywhere without swap: heaps, stacks, the compiler's working data. memory.stat reports it as anon. That, plus a smaller amount of kernel memory (37 to 136 MiB in these builds), is what has to fit. Our sampler read it every 100 ms, so it can miss a shorter spike. memory.peak and the max counter cannot miss one, and they show that no build except Vite at 2 GB ever reached its limit. The safe test is still the one we ran: the build inside the limit you plan to buy.
The Go build of the GitHub CLI shows the difference well. Its peak was 1,775 MiB. Anonymous memory never went over 424 MiB; about 1,400 MiB was page cache from the module sources and the build cache that go build writes. At the 2 GB limit, the same build finished with the same peak, because 1,775 MiB is still under 2,048 MiB. With a smaller limit the kernel may reclaim that cache instead of failing the build, but we did not test below 2 GB, so test the lower limit before you rely on it.
So if a dashboard shows that your CI job "peaked at 1.8 GB", that does not mean a 2 GB runner is too small. Look at anon in memory.stat (or RSS in your container metrics), and then add a margin for the runner itself.
Trap 2: a production build that is also a type check
The Excalidraw build needed far more memory than anything else: 3,248 MiB of anonymous memory, against 1,139 MiB for a full type check of the same code. The reason is in its Vite config:
checker({
typescript: true,
eslint:
envVars.VITE_APP_ENABLE_ESLINT === "false"
? undefined
: { lintCommand: 'eslint "./**/*.{js,ts,tsx}"' },
// ...
}),
vite-plugin-checker runs the TypeScript checker during vite build too: in version 0.7.2, which Excalidraw pins, enableBuild defaults to true. The production .env turns ESLint off but leaves the type check on, and the build also writes source maps. So one vite build is Rollup, source maps and a full tsc run at the same time.
At 2 GB the kernel killed it in all 3 runs, 69 to 122 seconds in, after the log said ✓ 1442 modules transformed. The kernel log names the cgroup and the process it chose:
oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null),cpuset=p1-vite-2G-r2.scope,mems_allowed=0,oom_memcg=/bench.slice/p1-vite-2G-r2.scope,task_memcg=/bench.slice/p1-vite-2G-r2.scope,task=MainThread,pid=77242,uid=0
Memory cgroup out of memory: Killed process 77242 (MainThread) total-vm:11451308kB, anon-rss:1138988kB, file-rss:4864kB, shmem-rss:0kB, UID:0 pgtables:29992kB oom_score_adj:0
Our wrapper's record of that run shows the kill. memory.events counted 39,825 times that the cgroup was about to go over its limit, and one OOM kill:
vite build, 2 GB limit, run 2
$ cat /root/res/p1/vite/2G/r2/result.env
rc=1
t0=1793637034.083018067
t1=1793637156.285208072
peak=2147483648
current_after=867889152
anon_after=801075200
file_after=38674432
$ cat /root/res/p1/vite/2G/r2/memory.events
low 0
high 0
max 39825
oom 1
oom_kill 1
oom_group_kill 0
The process list right after the kill shows the second half of the build. The checker runs tsc as its own child process, and it was still running after Vite had died:
77265 1808 /bin/sh -c tsc --noEmit
77266 799108 node /root/src/excalidraw/node_modules/.bin/tsc --noEmit
That tsc held 799,108 KiB (780 MiB) of resident memory under the 2 GB limit. Yarn reported Command failed with exit code 1, but the type check kept going until our harness stopped the scope. On a real runner it would keep using memory and CPU until the job ends.
We did not split the 3,248 MiB peak at 8 GB between Rollup and the checker, so we cannot say exactly how much the type check adds. What we can say: if your bundler runs a checker, your build step needs the memory of both, and moving the type check to its own CI job is the first thing to try before you buy a bigger runner.
Trap 3: runtimes size their heaps from the limit
Node and the JVM both read the cgroup limit when they start and size their default heap from it. Our harness asks each runtime what it would use inside each limit:
probe inside a 2 GB scope
# what Node 24 and JDK 21 pick inside MemoryMax=2G (from p1.sh probe)
$ cat /root/res/p1/probe-2G.txt
nproc=4
memory.max=2147483648
node_heap_size_limit=1174405120
int ActiveProcessorCount = -1 {product} {default}
size_t MaxHeapSize = 536870912 {product} {ergonomic}
double MaxRAMPercentage = 25.000000 {product} {default}
All four places side by side:
| Inside | Node 24 heap_size_limit | JDK 21 default MaxHeapSize |
| --- | --- | --- |
| 2 GB scope | 1,120 MiB | 512 MiB |
| 4 GB scope | 2,240 MiB | 1,024 MiB |
| 8 GB scope | 2,240 MiB | 2,048 MiB |
| whole droplet, no scope | 2,240 MiB | 1,986 MiB |
Two consequences showed up in the runs.
Heap use follows the limit. tsc --extendedDiagnostics prints Memory used, which is Node's heapUsed at the end of the run, in units of 1,000 bytes. With the whole machine, the type check of 1,508 files reported Memory used: 1018378K (median of 3, about 971 MiB). Inside 2 GB it reported 886903K (about 846 MiB), anonymous memory fell from 1,139 to 970 MiB, and the check finished about 24% slower (median 72.7 s against 58.8 s). The 2 GB run was 9.5 to 14.2 seconds slower in each of the three rounds. In rounds 2 and 3, where we logged the CPU counters, its steal time was only 4 to 7 points higher, so steal explains only part of the gap. More frequent garbage collection under the smaller heap limit would explain the rest, but we did not record GC activity, so that is a hypothesis. A big heap number at a high limit says little about what the program needs. We expected this check to fail at 2 GB with a heap error, and it did not.
Node stopped at 2,240 MiB even with more memory. On our 8 GB machine, Node's default heap limit was 2,240 MiB with a 4 GB scope, an 8 GB scope and no scope at all. We did not test a bigger machine, so check what your runner gives you with node -e 'console.log(require("v8").getHeapStatistics().heap_size_limit)'. If a build reports JavaScript heap out of memory, that is Node's own limit, not the kernel's. Set NODE_OPTIONS=--max-old-space-size=<MiB> explicitly instead of only buying more memory.
The JVM takes 25% of the limit by default (MaxRAMPercentage=25), so a Maven build or a test JVM without -Xmx gets 512 MiB inside 2 GB. Gradle sets its own daemon heap, which brings us to the next trap.
Trap 4: the Gradle daemon outlives the build
PlantUML's gradle.properties sets org.gradle.jvmargs=-Xmx1g. That cap kept the build inside 2 GB: anonymous memory peaked at about 1,143 MiB with the daemon and 1,144 MiB without it, and neither came near the limit.
The difference is what happens after gradlew exits. With the daemon, which is the default, the JVM stays alive and keeps its heap. Five seconds after the build, the scope still held 1,378 MiB, 1,048 MiB of it anonymous, in one process:
37292 1105976 /usr/lib/jvm/java-21-openjdk-amd64/bin/java --add-opens=java.base/java.lang=ALL-UNNAMED ...
With --no-daemon, Gradle printed To honour the JVM settings for this build a single-use Daemon process will be forked and Daemon will be stopped at the end of the build, and five seconds after exit the anonymous memory in the scope was under 1 MiB.
Anonymous memory around the end of the Gradle build (8 GB limit, run 1)
| | -55 s | -50 s | -45 s | -40 s | -35 s | -30 s | -25 s | -20 s | -15 s | -10 s | -5 s | 0 s | +5 s |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| daemon (default) | 637 MiB | 857 MiB | 927 MiB | 982 MiB | 1053 MiB | 1059 MiB | 1066 MiB | 1073 MiB | 1082 MiB | 1110 MiB | 1115 MiB | 1143 MiB | 1054 MiB |
| --no-daemon | 476 MiB | 516 MiB | 559 MiB | 807 MiB | 921 MiB | 970 MiB | 1067 MiB | 1072 MiB | 1080 MiB | 1084 MiB | 1112 MiB | 1144 MiB | 0 MiB |
_Max of 100 ms samples in each 5 s bin, labelled by the end of the bin; 0 s is the moment gradlew exits, +5 s is the 5 s after. One run each, PlantUML jar task, JDK 21, Gradle 9.3.1._
On a CI runner, that leftover daemon sits in the same memory as every later step of the job: the Docker build, the integration tests, the upload. At 8 GB the median time was 123.5 s with the daemon and 87.5 s without (112.7 s and 99.7 s at 2 GB), The ranges overlap and CPU steal was high, so we see no repeatable speed-up from the daemon for one Gradle command per job. The daemon is built to pay off across several Gradle commands in the same job, which we did not test. If a job runs one Gradle command, use --no-daemon (or org.gradle.daemon=false). If it runs several, consider keeping the daemon (we did not test that case), and run ./gradlew --stop before the steps that need the memory.
Trap 5: a limit on the docker CLI does not limit the build
docker build is a client. In our setup the work ran in a BuildKit container (Buildx's docker-container driver), which has its own cgroup. Other drivers and CI executors lay out cgroups differently, but the CLI is only a client in all of them. We wrapped the docker buildx build command in the same kind of scope as everything else, and at every limit that scope peaked at 39 to 42 MiB: the CLI, streaming logs.
To measure the build, we put the limit on the BuildKit container itself:
docker buildx create --name p1d8gr1 --driver docker-container \
--driver-opt network=host --driver-opt image=moby/buildkit:v0.34.1 \
--driver-opt memory=8g --driver-opt memory-swap=8g --driver-opt cpuset-cpus=0-3 \
--buildkitd-config buildkitd.toml --bootstrap
and read memory.peak from /sys/fs/cgroup/system.slice/docker-<container id>.scope. That container peaked at 2,072 MiB, of which only 355 MiB was anonymous: the Go compiler inside the build. Most of the rest was page cache from the image layers and the build's output, plus 136 MiB of kernel memory.
This matters for runner sizing in two ways. Your CI's memory graph may not show the build at all, depending on whether it measures the job's process tree or the whole machine. And if a self-hosted runner limits only the job's process tree, a Docker build in that job can run outside the limit, so a "2 GB job" can push the host far past 2 GB. Check which cgroup your executor puts the Docker daemon in.
Two method bugs that cost us a run
The first 2 GB Vite run left no result at all. The kernel killed the Vite process, and then systemd stopped the whole scope, including our wrapper script, before it could write anything. The journal said so:
p1-vite-2G-r1.scope: A process of this unit has been killed by the OOM killer.
p1-vite-2G-r1.scope: Failed with result 'oom-kill'.
systemd 255 sets DefaultOOMPolicy=stop, and scopes follow it. We set DefaultOOMPolicy=continue before the next 2 GB run (the published p1.sh also passes -p OOMPolicy=continue), and checked with a 64 MiB scope that the wrapper now survives a kill and records oom_kill 1. Runs 2 and 3 at 2 GB have full records.
Then our rerun of run 1 failed too, for a different reason. systemd keeps a failed transient unit loaded, so starting a new scope with the same name was refused:
Failed to start transient scope unit: Unit p1-vite-2G-r1.scope was already loaded or has a fragment file.
By the time we saw that, the Docker cache tests for the next post were running on the same droplet, so we did not try again. Run 1 still counts as a kill: its kernel log line reads oom_memcg=/bench.slice/p1-vite-2G-r1.scope,...,task=MainThread,pid=36041, and our sampler, which runs outside the scope, saw the cgroup reach 2,048 MiB, with up to 1,985 MiB anonymous, before its last sample at 70.5 seconds. The published redo mode now runs systemctl reset-failed first.
If you measure builds this way, set OOMPolicy=continue and give each run a unique unit name, or the runs that you care about most, the ones that fail, will be the ones with no data.
A sizing table
From these runs, for a 4 vCPU runner. The "smallest limit that passed" column is measured. The last column is our rule of thumb, not a measurement: a real VM also holds the OS, the runner agent and any service containers, so we pick the smallest size that leaves at least 1 GB above the anonymous peak.
| Build like ours | Anonymous peak | Smallest limit that passed every run | Runner we would pick |
| --- | --- | --- | --- |
| Bundler that also type-checks (Excalidraw vite build) | 3,248 MiB | 4 GB | 8 GB (moving the type check to its own job may make 4 GB enough; we did not test that) |
| Large type check on its own (tsc, 1,508 files) | 1,139 MiB at 8 GB, 970 MiB at 2 GB | 2 GB | 4 GB |
| Gradle with a 1 GB daemon heap (PlantUML jar) | 1,143 MiB | 2 GB | 4 GB, with --no-daemon |
| Go build of a mid-size CLI (gh) | 424 MiB | 2 GB | 2 GB |
| Docker build of a Go service | 355 MiB inside BuildKit | 2 GB | 2 GB, plus what the Docker daemon and the other steps need |
Hosted CI services list their machines by vCPU and memory, so you can match a build to a size. Bitrise, for example, lists every Linux machine type on its pricing page with its vCPU count and memory, such as 2 vCPU with 8 GB, and also sells a remote build cache for Gradle, Bazel and Xcode (checked on its public pages on 2 November 2026). Whatever you use, measure anonymous memory first, then pick the size.
What we could not conclude
- Three limits on one machine are not three machines. A real 2 GB runner has less page cache to spare, its own kernel, and a runner agent in the same memory. Leave a margin above the anonymous peak; our sizing table uses 1 GB.
- CPU count changes memory. Every run had 4 vCPUs. The Go compiler, Rollup's workers and Gradle's workers scale with CPUs, so these numbers are not valid for 2 or 16 vCPUs.
- Wall times are noisy. The droplet reported 23.5% CPU steal time across the 33 runs that our CPU counter log covered, and between 3.7% and 35.5% in single runs. The Go build took between 121.3 and 199.6 seconds at limits it never came close to. We do not read anything into a time difference between limits unless it repeats in every round, as the tsc gap did.
- Two or three runs per cell is enough to see a failure repeat and a peak hold steady, and that is all. The anonymous peaks varied by up to 21% for the Go build (353 to 441 MiB at 8 GB) and up to 15% for Gradle, and 6% or less for Vite, tsc and Docker between runs of the same cell.
- We did not split shared builds. We cannot say how much of the Vite build's memory is Rollup and how much is the checker, or how much of the Docker build's page cache is layer extraction.
- Rust ran once. One ripgrep build at 8 GB, so no repeats and no 2 GB or 4 GB data.
Run it yourself
Create a 4 vCPU, 8 GB Ubuntu 24.04 machine, copy the files below to /root/bench/, and run:
bash /root/bench/setup.sh
bash /root/bench/p1.sh warmup # probes the runtimes, one unmeasured run per build
bash /root/bench/p1.sh matrix # 3 rounds x 3 limits x each build (we stopped it after round 2)
bash /root/bench/p1.sh round3 # what we ran as round 3: 8 GB and 2 GB only
bash /root/bench/p1.sh redo # reruns any cell that left no result
python3 summarize_p1.py /root/res/p1
Each run writes its own directory with result.env, memory.events, samples.json and the build log, so you can read any single run on its own.
#setup.sh: toolchains, pinned sources, dependencies, registries
#!/bin/bash
# Droplet setup: toolchains, pinned sources, dependency prefetch, local registries.
set -euxo pipefail
export DEBIAN_FRONTEND=noninteractive
mkdir -p /root/src /root/res
cloud-init status --wait || true # first boot runs apt; wait for its lock
fetch_sha() { git init -q "$1"; git -C "$1" remote add origin "$2"; git -C "$1" fetch -q --depth 1 origin "$3"; git -C "$1" checkout -q FETCH_HEAD; }
fetch_sha /root/src/excalidraw https://github.com/excalidraw/excalidraw 8bcdca4526867433f99fdfcb55a063668db0c9dc
fetch_sha /root/src/plantuml https://github.com/plantuml/plantuml db06d70e18e71839dd5e330ac4086f3845371e3b
fetch_sha /root/src/cli https://github.com/cli/cli 9490d204a2ba95434153dedce99898ce9f281426
git clone -q --depth 1 --branch 15.2.0 https://github.com/BurntSushi/ripgrep /root/src/ripgrep
fetch_sha /root/src/msd https://github.com/GoogleCloudPlatform/microservices-demo c7cb6d5bfd6ea3aed768634ebbcb98cb7a523712
curl -fsSL https://go.dev/dl/go1.27.3.linux-amd64.tar.gz | tar -C /usr/local -xz
curl -fsSL https://nodejs.org/dist/v24.22.2/node-v24.22.2-linux-x64.tar.xz | tar -C /usr/local --strip-components=1 -xJ
curl -fsSL https://sh.rustup.rs | sh -s -- -y -q --profile minimal --default-toolchain stable
apt-get update -q
apt-get install -y -q openjdk-21-jdk-headless build-essential git jq zstd python3 ca-certificates curl
curl -fsSL https://get.docker.com | sh
export PATH=/usr/local/go/bin:/root/.cargo/bin:/usr/local/bin:$PATH GOTOOLCHAIN=local
npm install -g yarn@1.22.22
docker pull -q moby/buildkit:v0.34.1
docker pull -q registry:2.8.3
docker run -d --name reg-cache -p 127.0.0.1:5000:5000 registry:2.8.3
docker run -d --name mirror-hub -p 127.0.0.1:5001:5000 -e REGISTRY_PROXY_REMOTEURL=https://registry-1.docker.io registry:2.8.3
docker run -d --name mirror-gcr -p 127.0.0.1:5002:5000 -e REGISTRY_PROXY_REMOTEURL=https://gcr.io registry:2.8.3
# dependencies are downloaded once, outside the measured runs
(cd /root/src/excalidraw && yarn install --frozen-lockfile --network-timeout 600000)
(cd /root/src/cli && go mod download)
(cd /root/src/ripgrep && cargo fetch --locked)
# systemd stops a scope after an OOM kill by default; keep the wrapper alive to record the result
mkdir -p /etc/systemd/system.conf.d
printf '[Manager]\nDefaultOOMPolicy=continue\n' > /etc/systemd/system.conf.d/oom.conf
systemctl daemon-reexec
#wrap.sh: runs inside the scope and records the result
#!/bin/bash
# Runs inside the systemd scope. usage: wrap.sh OUTDIR TIMEOUT_S cmd...
out=$1; tmo=$2; shift 2
cg=/sys/fs/cgroup$(sed -n 's/^0:://p' /proc/self/cgroup)
echo "$cg" > "$out/cgroup"
cat "$cg/memory.max" > "$out/memory.max"
t0=$(date +%s.%N)
timeout -k 15 "$tmo" "$@" > "$out/build.log" 2>&1
rc=$?
t1=$(date +%s.%N)
{
echo "rc=$rc"
echo "t0=$t0"
echo "t1=$t1"
echo "peak=$(cat "$cg/memory.peak")"
echo "current_after=$(cat "$cg/memory.current")"
awk '$1=="anon"||$1=="file"{print $1"_after="$2}' "$cg/memory.stat"
} > "$out/result.env"
cp "$cg/memory.events" "$out/memory.events"
for p in $(cat "$cg/cgroup.procs"); do
["$p" = "$$"] && continue
ps -o pid=,rss=,args= -p "$p" | cut -c1-400
done > "$out/leftover.txt" 2>/dev/null
if [-n "${LEFTOVER_WAIT:-}"]; then
sleep "$LEFTOVER_WAIT"
echo "current_later=$(cat "$cg/memory.current")" >> "$out/result.env"
awk '$1=="anon"{print "anon_later="$2}' "$cg/memory.stat" >> "$out/result.env"
for p in $(cat "$cg/cgroup.procs"); do
["$p" = "$$"] && continue
ps -o pid=,rss=,args= -p "$p" | cut -c1-400
done > "$out/leftover_later.txt" 2>/dev/null
fi
touch "$out/done"
exit $rc
#sample.py: polls memory.stat every 100 ms from outside the scope
#!/usr/bin/env python3
"""Poll one cgroup v2 directory every 100 ms until a done file appears.
usage: sample.py CGROUP_DIR OUT_JSON DONE_FILE
Records the maximum of anon, file, kernel and memory.current, plus a series.
"""
import json, os, sys, time
cg, out, done = sys.argv[1:4]
start = time.time()
while not os.path.isdir(cg):
if time.time() - start > 60 or os.path.exists(done):
json.dump({"error": "cgroup never appeared"}, open(out, "w"))
sys.exit(0)
time.sleep(0.02)
keys = ("anon", "file", "kernel", "shmem")
mx = {k: 0 for k in keys}
mx["current"] = 0
series = []
t0 = time.time()
n = 0
while not os.path.exists(done):
try:
with open(os.path.join(cg, "memory.stat")) as f:
st = {}
for line in f:
k, v = line.split()
if k in keys:
st[k] = int(v)
with open(os.path.join(cg, "memory.current")) as f:
cur = int(f.read())
except OSError:
break
n += 1
for k in keys:
mx[k] = max(mx[k], st.get(k, 0))
mx["current"] = max(mx["current"], cur)
series.append([round(time.time() - t0, 2), st.get("anon", 0), st.get("file", 0), cur])
time.sleep(0.1)
json.dump({"max": mx, "samples": n, "interval_s": 0.1, "series": series}, open(out, "w"))
#p1-cmds.sh: the builds, and what is deleted before each run
#!/bin/bash
# Build definitions for post 1. prep_* runs outside the cgroup, cmd_* inside it.
export PATH=/usr/local/go/bin:/root/.cargo/bin:/usr/local/bin:$PATH GOTOOLCHAIN=local
EX=/root/src/excalidraw PU=/root/src/plantuml GH=/root/src/cli RG=/root/src/ripgrep
FRONTEND=/root/src/msd/src/frontend
stop_gradle() { pkill -f GradleDaemon || true; pkill -f GradleWrapperMain || true; pkill -f KotlinCompileDaemon || true; sleep 1; }
prep_vite() { rm -rf $EX/excalidraw-app/build $EX/node_modules/.vite $EX/excalidraw-app/node_modules/.vite; }
cmd_vite() { cd $EX && yarn build:app:docker; }
prep_tsc() { find $EX -maxdepth 2 -name '*.tsbuildinfo' -delete; }
cmd_tsc() { cd $EX && node_modules/.bin/tsc -p tsconfig.json --extendedDiagnostics; }
prep_gradle_daemon() { stop_gradle; rm -rf $PU/build $PU/.gradle; }
cmd_gradle_daemon() { cd $PU && ./gradlew jar --console=plain --no-build-cache; }
prep_gradle_nodaemon() { stop_gradle; rm -rf $PU/build $PU/.gradle; }
cmd_gradle_nodaemon() { cd $PU && ./gradlew --no-daemon jar --console=plain --no-build-cache; }
prep_go() { rm -rf /root/gocache /root/out; mkdir -p /root/out; }
cmd_go() { cd $GH && GOCACHE=/root/gocache go build -o /root/out/gh ./cmd/gh; }
prep_rust() { rm -rf $RG/target; }
cmd_rust() { cd $RG && cargo build --release --locked; }
prep_docker() { docker buildx ls --format '{{.Name}}' 2>/dev/null | grep '^p1d' | xargs -r -n1 docker buildx rm >/dev/null 2>&1 || true; }
if ["${BASH_SOURCE[0]}" = "$0" ]; then "$@"; fi
#p1.sh: probe, warm-up, matrix, round 3, redo, and the docker build cell
#!/bin/bash
# Post 1 matrix. usage: p1.sh warmup | p1.sh matrix
set -u
B=/root/bench R=/root/res/p1
mkdir -p $R
source $B/p1-cmds.sh
LIMITS=(8G 4G 2G)
BUILDS=(vite tsc gradle_daemon gradle_nodaemon go rust docker)
# rust ran once (warmup) only: the matrix did not fit the droplet time budget
MATRIX_BUILDS=(vite tsc gradle_daemon gradle_nodaemon go docker)
CPUS=0-3
log() { echo "$(date -u +%FT%TZ) $*" | tee -a $R/progress.log; }
scope_run() { # unit limit out tmo cmd...
local unit=$1 lim=$2 out=$3 tmo=$4; shift 4
python3 $B/sample.py /sys/fs/cgroup/bench.slice/$unit.scope $out/samples.json $out/done &
local sp=$!
systemd-run --scope --quiet --slice=bench.slice --unit=$unit \
-p MemoryMax=$lim -p MemorySwapMax=0 -p AllowedCPUs=$CPUS -p OOMPolicy=continue \
$B/wrap.sh $out $tmo "$@"
wait $sp
systemctl stop $unit.scope 2>/dev/null || true
}
docker_cell() { # lim out tmo tag
local lim=$1 out=$2 tmo=$3 tag=$4
local name=$(echo p1d$tag | tr A-Z a-z) m=$(echo $1 | tr G g)
docker buildx create --name $name --driver docker-container \
--driver-opt network=host --driver-opt image=moby/buildkit:v0.34.1 \
--driver-opt memory=$m --driver-opt memory-swap=$m --driver-opt cpuset-cpus=$CPUS \
--buildkitd-config $B/buildkitd.toml --bootstrap > $out/create.log 2>&1
local cid; cid=$(docker inspect -f '{{.Id}}' buildx_buildkit_${name}0)
local cg=/sys/fs/cgroup/system.slice/docker-$cid.scope
cat $cg/memory.max > $out/container_memory.max
mkdir -p $out/cli
python3 $B/sample.py $cg $out/container_samples.json $out/cli/done &
local sp=$!
scope_run p1-docker-$tag $lim $out/cli $tmo \
docker buildx build --builder $name --progress=plain --provenance=false --output type=cacheonly $FRONTEND
wait $sp
{ echo "container_peak=$(cat $cg/memory.peak)"; cat $out/cli/result.env; } > $out/result.env
cp $cg/memory.events $out/container_memory.events
cp $out/cli/build.log $out/build.log
docker buildx rm $name > /dev/null 2>&1
}
run_cell() { # build limit run tmo
local b=$1 lim=$2 r=$3 tmo=$4
local tag=$b-$lim-$r out=$R/$b/$lim/$r
rm -rf $out; mkdir -p $out
prep_$b
sync; echo 3 > /proc/sys/vm/drop_caches; sleep 2
if [$b = docker]; then
docker_cell $lim $out $tmo ${lim}$r
else
case $b in gradle_*) export LEFTOVER_WAIT=5 ;; esac
scope_run p1-$tag $lim $out $tmo bash $B/p1-cmds.sh cmd_$b
unset LEFTOVER_WAIT
fi
[$b = gradle_daemon] || [$b = gradle_nodaemon] && stop_gradle
log "$tag $(tr '\n' ' ' < $out/result.env)"
}
probe() { # record what the runtimes see inside each limit
for lim in "${LIMITS[@]}"; do
systemd-run --scope --quiet --slice=bench.slice --unit=probe-$lim -p MemoryMax=$lim -p MemorySwapMax=0 -p AllowedCPUs=$CPUS \
bash -c 'echo "nproc=$(nproc)"; echo "memory.max=$(cat /sys/fs/cgroup$(sed -n "s/^0:://p" /proc/self/cgroup)/memory.max)";
echo "node_heap_size_limit=$(node -e "console.log(require(\"v8\").getHeapStatistics().heap_size_limit)")";
java -XX:+PrintFlagsFinal -version 2>/dev/null | grep -E " (MaxHeapSize|MaxRAMPercentage|ActiveProcessorCount) "' > $R/probe-$lim.txt 2>&1
done
{ echo "nproc=$(nproc)"; free -b | head -2; echo "node_heap_size_limit=$(node -e 'console.log(require("v8").getHeapStatistics().heap_size_limit)')";
java -XX:+PrintFlagsFinal -version 2>/dev/null | grep -E " (MaxHeapSize|MaxRAMPercentage) "; } > $R/probe-host.txt 2>&1
}
case ${1:-} in
warmup)
probe
for b in "${BUILDS[@]}"; do run_cell $b 8G r0 900; done
log WARMUP-DONE ;;
matrix)
declare -A TMO
for b in "${MATRIX_BUILDS[@]}"; do
. <(grep -E '^t[01]=' $R/$b/8G/r0/result.env)
w=$(python3 -c "print(int(float('$t1')-float('$t0')))")
t=$((3 * w + 60)); [$t -gt 600] && t=600; [$t -lt 120] && t=120
TMO[$b]=$t
done
log "timeouts: $(for b in "${MATRIX_BUILDS[@]}"; do printf '%s=%s ' $b ${TMO[$b]}; done)"
for r in r1 r2 r3; do
for b in "${MATRIX_BUILDS[@]}"; do
for lim in "${LIMITS[@]}"; do run_cell $b $lim $r ${TMO[$b]}; done
done
done
log MATRIX-DONE ;;
round3) # third run at 8G and 2G only: the full round did not fit the droplet time budget
declare -A TMO=([vite]=300 [tsc]=381 [gradle_daemon]=600 [gradle_nodaemon]=351 [go]=414 [docker]=351)
log "round 3 at 8G and 2G only"
for b in "${MATRIX_BUILDS[@]}"; do
for lim in 8G 2G; do run_cell $b $lim r3 ${TMO[$b]}; done
done
log MATRIX-DONE ;;
redo) # rerun cells that left no result (systemd stopped the scope after an OOM kill)
declare -A TMO=([vite]=300 [tsc]=381 [gradle_daemon]=600 [gradle_nodaemon]=351 [go]=414 [docker]=351)
for b in "${MATRIX_BUILDS[@]}"; do for lim in "${LIMITS[@]}"; do for r in r1 r2 r3; do
d=$R/$b/$lim/$r
if [-d $d] && ! grep -q '^rc=' $d/result.env 2>/dev/null; then
rm -rf $d.lost; mv $d $d.lost; log "redo $b $lim $r"
systemctl reset-failed p1-$b-$lim-$r.scope 2>/dev/null # added after our run: a failed scope blocks its name
run_cell $b $lim $r ${TMO[$b]}
fi
done; done; done
log REDO-DONE ;;
*) echo "usage: $0 warmup|matrix|round3|redo"; exit 2 ;;
esac
#buildkitd.toml: send base image pulls to the local mirror
[registry."docker.io"]
mirrors = ["127.0.0.1:5001"]
[registry."gcr.io"]
mirrors = ["127.0.0.1:5002"]
[registry."127.0.0.1:5000"]
http = true
insecure = true
[registry."127.0.0.1:5001"]
http = true
insecure = true
[registry."127.0.0.1:5002"]
http = true
insecure = true
#summarize_p1.py: medians per build and limit
#!/usr/bin/env python3
"""Summarize post 1 runs: medians per build and limit over the measured runs (r1..r3)."""
import json, os, re, statistics, sys
root = sys.argv[1]
BUILDS = ["vite", "tsc", "gradle_daemon", "gradle_nodaemon", "go", "rust", "docker"]
LIMITS = ["8G", "4G", "2G"]
MiB = 2 ** 20
def env(path):
d = {}
if os.path.exists(path):
for line in open(path):
if "=" in line:
k, v = line.strip().split("=", 1)
d[k] = v
return d
def events(path):
d = {}
if os.path.exists(path):
for line in open(path):
k, v = line.split()
d[k] = int(v)
return d
def run_info(d, build):
r = env(os.path.join(d, "result.env"))
if not r or "rc" not in r:
return None
log = open(os.path.join(d, "build.log"), errors="replace").read() if os.path.exists(os.path.join(d, "build.log")) else ""
if build == "docker":
if not r.get("container_peak"):
return None # warm-up run 0 recorded no container numbers (builder name case bug)
ev = events(os.path.join(d, "container_memory.events"))
smp = json.load(open(os.path.join(d, "container_samples.json")))
peak = int(r["container_peak"])
cli_peak = int(r["peak"])
else:
ev = events(os.path.join(d, "memory.events"))
smp = json.load(open(os.path.join(d, "samples.json")))
peak = int(r["peak"])
cli_peak = None
rc = int(r["rc"])
wall = float(r["t1"]) - float(r["t0"])
if rc == 0:
outcome = "ok"
elif ev.get("oom_kill", 0) > 0:
outcome = "oom_kill"
elif "heap out of memory" in log or "Reached heap limit" in log:
outcome = "node_heap"
elif "OutOfMemoryError" in log:
outcome = "java_heap"
elif rc in (124, 137):
outcome = "timeout"
else:
outcome = f"fail_rc{rc}"
mx = smp.get("max", {})
return {
"rc": rc, "outcome": outcome, "wall_s": round(wall, 1),
"peak_mib": round(peak / MiB), "anon_max_mib": round(mx.get("anon", 0) / MiB),
"file_max_mib": round(mx.get("file", 0) / MiB), "kernel_max_mib": round(mx.get("kernel", 0) / MiB),
"samples": smp.get("samples"),
"oom_kill": ev.get("oom_kill", 0), "max_events": ev.get("max", 0), "high_events": ev.get("high", 0),
"after_mib": round(int(r.get("current_after", 0)) / MiB), "anon_after_mib": round(int(r.get("anon_after", 0)) / MiB),
"cli_peak_mib": round(cli_peak / MiB) if cli_peak is not None else None,
"later_mib": round(int(r["current_later"]) / MiB) if "current_later" in r else None,
"anon_later_mib": round(int(r["anon_later"]) / MiB) if "anon_later" in r else None,
"leftover_later": open(os.path.join(d, "leftover_later.txt")).read().strip() if os.path.exists(os.path.join(d, "leftover_later.txt")) else None,
"leftover": open(os.path.join(d, "leftover.txt")).read().strip() if os.path.exists(os.path.join(d, "leftover.txt")) else "",
}
def med(xs):
xs = [x for x in xs if x is not None]
return round(statistics.median(xs), 1) if xs else None
out = {}
for b in BUILDS:
for lim in LIMITS:
runs = {}
for rn in ("r0", "r1", "r2", "r3"):
d = os.path.join(root, b, lim, rn)
if os.path.isdir(d):
info = run_info(d, b)
if info:
runs[rn] = info
meas = [runs[k] for k in ("r1", "r2", "r3") if k in runs]
ok = [x for x in meas if x["outcome"] == "ok"]
out[f"{b}/{lim}"] = {
"runs": runs, "n": len(meas), "n_ok": len(ok),
"outcomes": [x["outcome"] for x in meas],
"wall_med_ok": med([x["wall_s"] for x in ok]),
"wall_med_all": med([x["wall_s"] for x in meas]),
"peak_med": med([x["peak_mib"] for x in meas]),
"anon_med": med([x["anon_max_mib"] for x in meas]),
"file_med": med([x["file_max_mib"] for x in meas]),
"peak_range": [min(x["peak_mib"] for x in meas), max(x["peak_mib"] for x in meas)] if meas else None,
"anon_range": [min(x["anon_max_mib"] for x in meas), max(x["anon_max_mib"] for x in meas)] if meas else None,
"wall_range": [min(x["wall_s"] for x in meas), max(x["wall_s"] for x in meas)] if meas else None,
"after_med": med([x["after_mib"] for x in meas]),
"anon_after_med": med([x["anon_after_mib"] for x in meas]),
"cli_peak_med": med([x["cli_peak_mib"] for x in meas]),
"later_med": med([x["later_mib"] for x in meas]),
"anon_later_med": med([x["anon_later_mib"] for x in meas]),
"max_events_med": med([x["max_events"] for x in meas]),
}
json.dump(out, open(os.path.join(root, "summary.json"), "w"), indent=1)
print(f"{'cell':26} {'n':>2} {'ok':>2} {'wall_ok':>8} {'peak':>6} {'anon':>6} {'file':>6} {'after':>6} outcomes / ranges")
for k, v in out.items():
print(f"{k:26} {v['n']:>2} {v['n_ok']:>2} {str(v['wall_med_ok']):>8} {str(v['peak_med']):>6} {str(v['anon_med']):>6} {str(v['file_med']):>6} {str(v['after_med']):>6} {v['outcomes']} wall{v['wall_range']} anon{v['anon_range']} peak{v['peak_range']} maxev={v['max_events_med']} cli={v['cli_peak_med']} later={v['later_med']}/{v['anon_later_med']}")
Summary
Most CI builds need less memory than the cgroup counter suggests, because memory.peak includes page cache that the kernel gives back under pressure. Measure anonymous memory, run the build inside the limit you want to buy, and check memory.events for max and oom_kill. Then look for the traps: a bundler that runs a type checker, a runtime that sizes its heap from the limit, a Gradle daemon that stays after the build, and a Docker build that runs outside your cgroup. In our runs, fixing any of those was worth more than doubling the runner's memory.
_Originally published at devops-daily.com._
5 reactions
Ищем нишу для продукта с нейросетями на примере Excalidraw
Осенью 2024 года Miro отключил платные аккаунты из России и Беларуси, бесплатные оставил. За два года на освободившееся место пришли Яндекс, VK, МТС, Битрикс24 и ещё с десяток российских сервисов. Поэтому, когда в октябре 2026-го на Хабре вышла новость о новых ограничениях Miro, её почти никто не стал обсуждать: пользователи к тому времени уже переехали.
Когда крупный сервис уходит с рынка, хочется сразу сделать ему замену. Но об уходе узнают все в один день, Яндекс тоже. Если делать просто ещё одну доску вместо Miro, придётся тягаться с корпорациями по бюджету, и шансов тут мало.
Мне стало интересно, что получится, если копать глубже. За вечер с ИИ я нашла сегмент, до которого у корпораций не доходят руки. Это команды, которые держат open source доску Excalidraw на своих серверах, любят её и при этом скучают по функциям Miro.
Статья для тех, кто пишет код и думает о своём продукте. Расскажу, какими нейросетями искать нишу, как разбирать источники и по каким фреймворкам оценивать идею, чтобы понять, принесёт ли она деньги, до того как вы потратите на неё полгода. Подход я собрала из практик Y Combinator, исследований McKinsey и Anthropic и проверила на примере с Excalidraw.
Почему не стоит брать идею из подборки
Пол Грэм, сооснователь Y Combinator, называет идеи, придуманные за столом, «ситкомными». В сериале они звучали бы правдоподобно, а настоящей потребности за ними нет. В его эссе есть пример с соцсетью для владельцев питомцев. Знакомые собачники вежливо скажут «может, и попробую», и на этом всё, пользователей не будет.
Подборки «идей для бизнеса» почти целиком состоят из таких идей. Их прочитали тысячи людей, а спрос никто не проверял. В Y Combinator для самых заманчивых ловушек даже есть термин tarpit ideas, идеи-трясины. Они кажутся простыми, на них слетается много основателей, и годами ничего не взлетает, потому что сложность спрятана в устройстве рынка.
С ИИ собрать продукт стало проще, поэтому теперь всё решает выбор, что именно собирать. В марте 2026 года Miro купил Reforge, платформу для обучения продуктовых команд. Её CEO Брайан Балфур объяснил сделку тем, что с ИИ командам нужны новые навыки, от прототипирования до стратегии.
В России ошибка стоит дороже. За 2025 год российские стартапы привлекли всего $159 млн, а сделок стало на 42% меньше. Так что проверять идею приходится на свои деньги и до того, как вкладываться всерьёз.
Что нейросети изменили в исследовании рынка
Раньше на исследование ниши уходили недели, иностранные источники мешал читать язык, а разбор данных стоил денег. Теперь большую часть этой работы берёт на себя нейросеть.
Агент глубокого исследования за время от 2 до 30 минут проходит десятки и сотни страниц и выдаёт отчёт со ссылками.
Китайский, английский и русский модель читает одинаково. Поэтому Xiaohongshu (что-то вроде Pinterest и Instagram в одном) и Zhihu (китайская Quora) становятся доступны без переводчика.
Тысячи отзывов модель сама раскладывает по темам, тональности и силе сигнала. Раньше на это сажали отдел аналитики.
В McKinsey проверили это на продакт-менеджерах. Часть сэкономленного времени пришлась как раз на сведение пользовательских исследований, когда команда ещё ищет идею.
Какие нейросети и инструменты брать
Версии моделей обновляются каждые пару месяцев, так что в таблице семейства моделей и задачи, для которых я их беру в октябре 2026 года.
Задача
Инструмент
Почему он
Быстро найти источники
Perplexity Deep Research
Отчёт за 2-4 минуты, ссылка почти у каждого утверждения
Большой структурный отчёт
ChatGPT Deep Research
Работает до 30 минут, отчёт выходит самый длинный и подробный
Максимум источников
Gemini Deep Research
Просматривает 100 страниц и больше за один запрос
Свести противоречивые данные в вывод
Claude Research
Главный агент планирует, подагенты ищут параллельно, а ссылки проверяет отдельный проход
Соцсети в реальном времени
Grok
Лучше других работает с данными из соцсетей
Русскоязычный контекст и данные внутри РФ
Алиса AI и YandexGPT в Yandex Cloud, GigaChat
Знают российские реалии. Сбер выложил веса крупных моделей GigaChat под лицензией MIT, их можно развернуть на своём сервере
Китайские источники и дешёвая массовая обработка
DeepSeek, Qwen
Понимают китайский, миллион токенов стоит несколько рублей
Сбор данных со страниц
Apify, Firecrawl, парсеры на Python
Выгружают отзывы, треды и карточки товаров в таблицу
Группировка тысяч отзывов
BERTopic
Тематическое моделирование на эмбеддингах
Одного инструмента мне обычно мало. Источники я ищу в Perplexity, а разбирать их и писать выводы отдаю Claude или ChatGPT. Такую связку советуют и в обзорах инструментов.
Как обрабатывать источники
Выгрузите отзывы, треды и issues в таблицу парсером или агентом. В таблице нужны текст, оценка, дата, источник и ссылка.
Уберите дубли, рекламу и отзывы без текста. Если источники на разных языках, переведите всё на один.
Разбейте отзывы на отдельные мнения. В одном отзыве часто несколько жалоб, и модель вытащит каждую с темой, тональностью и цитатой. Так же делают в научных работах по анализу отзывов.
Сгруппируйте мнения. Их переводят в эмбеддинги и кластеризуют, обычно связкой UMAP, HDBSCAN и BERTopic. Когда методы сравнивали на отзывах, самые осмысленные темы получились у BERTopic. После этого модель может дать каждой теме короткое понятное название.
Для каждой темы определите силу сигнала по таблице из шага 3, посчитайте, как часто она встречается, и посмотрите, растёт ли она со временем.
Откройте несколько исходных отзывов по каждой крупной теме. Модель любит уверенно обобщать и может найти тренд, которого нет.
Если отзывов несколько сотен, разбивку, группировку и оценку можно сделать одним промптом в чате. Для тысяч понадобится скрипт, но его тоже можно попросить у ИИ. Я начинала с такого промпта:
Вот 300 отзывов на 1-3 звезды о сервисах онлайн-досок. Для каждого выдели отдельные жалобы. Сгруппируй жалобы в темы. Для каждой темы дай название, число упоминаний, две короткие цитаты и пометку, пишет ли кто-то, что платит за обходное решение. Отсортируй по числу упоминаний. Не добавляй тем, которых нет в отзывах.
Шаг 1. Пропустите идею через 10 вопросов YC
Джаред Фридман, партнёр Y Combinator, собрал эти вопросы, когда разбирал, откуда взялись идеи у сотни лучших компаний YC. Оказалось, что минимум 70% из них пришли к идее сами, из собственного опыта, без всякого мозгового штурма.
Подходите ли вы для этого рынка? Фридман считает этот вопрос главным.
Насколько большой рынок сейчас и будет ли он расти?
Насколько острая проблема? Лучший случай, когда людям сейчас нечем её решить.
Есть ли конкуренты? Чаще это хороший знак, значит, спрос есть.
Хотите ли вы этот продукт сами? Знаете ли лично тех, кто хочет?
Стало ли это возможным или нужным недавно?
Есть ли похожие бизнесы, которые уже зарабатывают?
Готовы ли вы работать над этим годами?
Масштабируется ли бизнес?
Насколько удачна сама область идей?
Ещё Фридман пишет, что некоторые признаки только выглядят минусами. Если идею трудно начать, область кажется скучной или конкуренты уже есть, это скорее говорит в её пользу.
У Грэма есть ещё один критерий, про форму спроса. По его мнению, лучше сделать продукт, который очень нужен небольшой группе, чем продукт, который всем нужен чуть-чуть. Он сравнивает такой рынок с колодцем: узко, зато глубоко.
С нейросетями хорошо сочетаются ещё несколько методов.
Jobs to Be Done ищет задачу, ради которой человек «нанимает» продукт. Модель неплохо достаёт такие задачи из отзывов, если попросить её писать по шаблону «когда я..., я хочу..., чтобы...».
The Mom Test Роба Фитцпатрика учит спрашивать на интервью о прошлом поведении и потраченных деньгах, потому что мнение о вашей идее ничего не стоит. ИИ поможет составить вопросы и разобрать расшифровки.
Дерево возможностей Терезы Торрес (Opportunity Solution Tree) идёт от цели к возможностям и решениям и держится на регулярных разговорах с пользователями. Темы, которые нашла нейросеть, ложатся в ветки этого дерева.
Шаг 2. Найдите сигналы спроса
Спрос видно по жалобам, обходным путям, ручной работе и деньгам, которые люди уже платят. Вот где я ищу эти следы.
Больше всего, по-моему, недооценивают отзывы о конкурентах. Читать стоит отзывы на 1-3 звезды, там люди сами пишут, чего им не хватает и за что они готовы платить.
Из международных площадок это G2, Capterra, Trustpilot, Product Hunt, App Store и Google Play.
На китайском Xiaohongshu каждый популярный пост читается как отзыв или гайд по покупке. На Zhihu видно, какими словами люди описывают проблему и каких конкурентов сравнивают. Есть ещё отзывы на Taobao и Tmall. Аудитория там огромная, а язык ИИ переведёт.
Из российских подойдут RuStore, Wildberries и Ozon, Отзовик, IRecommend, Яндекс Карты для офлайн-бизнесов и вопросы на Хабр Q&A.
Я ищу повторы. Если одна и та же жалоба встречается раз двадцать, в продукте дыра.
Разработчики жалуются в GitHub issues. Смотрите те, что открыты годами и собрали много реакций. В Show HN на Hacker News по комментариям видно, что людей цепляет и что раздражает, а в Ask HN прямо спрашивают, каким инструментом решить задачу. На Reddit ищите фразы «I wish there was an app for» и «alternative to». Западные тренды доходят до нас с задержкой, и это даёт фору по времени.
Отдельная история, когда платформа меняет правила и пользователи остаются без инструментов. В июле 2025 года Chrome отключил расширения на Manifest V2 у всех пользователей, а с Chrome 139 они перестали работать совсем. У каждого такого расширения осталась аудитория. Можно зайти и с другой стороны и делать плагины и интеграции для растущих платформ, куда пользователи приходят быстрее, чем появляются инструменты.
Популярные таблицы и шаблоны на Etsy, Gumroad и в Notion показывают, что люди до сих пор делают руками. Easlo, например, продаёт шаблоны для Notion, и к 2024 году у него набрались десятки тысяч покупателей. Шаблон, который хорошо продаётся, часто оказывается прототипом будущего продукта.
Самый сильный сигнал даёт краудфандинг, потому что там люди платят за то, чего ещё нет. Kickstarter удобно смотреть как витрину спроса. Российская planeta.ru за год собрала 201 млн рублей, в основном на музыку и социальные проекты. На площадках вроде Microns продают небольшие готовые продукты вместе с пользователями и выручкой. А Y Combinator сам публикует темы, в которых видит спрос, в разделе Requests for Startups.
Соцсети показывают, чем люди увлечены прямо сейчас. Если договориться с креатором о партнёрстве, дистрибуция будет с первого дня, потому что его аудитория станет вашей. В России то же самое происходит в VK Клипах и Telegram-каналах.
Ещё один путь называют геоарбитражем. Продукт, который работает на одном рынке, переносят на другой и адаптируют под местные платежи и законы, у нас это СБП, 152-ФЗ и интеграция с 1С. В YC такой ориентир называют прокси. Так Rappi взял модель DoorDash и перенёс доставку еды в Латинскую Америку. Ушедшие сервисы тоже сюда относятся, просто этот сигнал видят все, и копать приходится глубже.
И последнее. Если задачу решают только длинным чек-листом шагов на сорок, нормального инструмента для неё нет.
Шаг 3. Оцените, будут ли платить
Сигналы бывают разной силы. В таблице они идут от сильных к слабым.
Сила
Сигнал
Что он значит
1
Предзаказ или оплата
Спрос подтверждён деньгами
2
Люди платят за решение у конкурента или за хостинг open source
Рынок есть, цена понятна
3
Обходной путь или ручная работа
Проблема болит настолько, что на неё тратят время
4
Повторяющиеся жалобы в отзывах
Проблема есть, готовность платить не проверена
5
Лайки и «было бы круто»
Почти ничего не значит
Потом я считаю на салфетке, сколько людей в сегменте, сколько они платят за ближайшую альтернативу и какую долю можно реально забрать за год. Если даже на зарплату одного человека не набирается, стоит искать сегмент глубже или соседний.
В B2B считают так же, только сигналы тише, а цикл сделки длиннее. Решение там принимает несколько человек: пользователь, руководитель, безопасность и закупка. Поэтому искать сигнал стоит у тех, кто работает с инструментом каждый день, и сразу проверять, есть ли у них бюджет и право голоса. Самые честные сигналы в B2B лежат в открытом доступе. По вакансиям конкурентов видно, куда они вкладываются, по тендерам видны реальные бюджеты, а по истории изменений в документации понятно, что просили клиенты.
Шаг 4. Разбираю нишу Excalidraw вместе с ИИ
Этот разбор занял у меня один вечер. Claude с веб-поиском искал и сводил источники, я проверяла. Раньше на такое уходило несколько недель.
Сначала я попросила найти, что люди пишут про Excalidraw на своих серверах. Вот что нашлось.
У проекта 135 тысяч звёзд на GitHub и лицензия MIT, так что на этом коде можно легально строить свой продукт.
Если поставить Excalidraw на свой сервер, совместное редактирование и ссылки из коробки не работают. Запрос на это висит в репозитории уже много лет.
Сообщество собирает свои сборки с совместной работой.
Хостинги продают развёртывание Excalidraw с совместной работой в один клик, от €9 в месяц. По таблице выше это сигнал второй силы, за решение уже платят.
Для воркшопов с таймерами и голосованием в сравнениях советуют Miro. В Excalidraw этих функций нет.
Потом я посмотрела на конкурентов.
У Яндекс Досок с февраля 2025 года есть версия для серверов крупных компаний, но эстетика у неё другая.
Excalidraw+ версию для своего сервера ещё разрабатывает, и она войдёт в корпоративную лицензию.
Для tldraw в продакшне нужен лицензионный ключ, а коммерческую лицензию продают через отдел продаж.
Из этого получилась гипотеза. Доска в эстетике Excalidraw с функциями для воркшопов, как в Miro, которую компания ставит на свой сервер в России. Если пройтись по вопросам YC, проблема острая для узкой группы, конкуренты есть, похожие бизнесы зарабатывают, а окно открылось недавно.
Факты ИИ собирает быстро, но оценивать их всё равно человеку. McKinsey проверили это на 40 продакт-менеджерах. С ИИ продуктивность выросла на 40%, опытные сохранили качество, а начинающие стали работать быстрее, но хуже. ИИ уверенно путает даты, версии и цены, поэтому каждый факт приходится сверять с первоисточником. В моём разборе часть данных в пересказах оказалась устаревшей, и на проверку ушла заметная часть вечера.
Шаг 5. Найдите преимущество, которое не скопируют
В стартапах такое преимущество называют moat, то есть ров. Конкурент не сможет быстро его повторить, даже если захочет.
Корпорация считает рынок миллиардами и отчитывается перед акционерами каждый квартал. Сегмент в несколько тысяч команд ей невыгоден, даже если она его заметила. А маленькая команда может годами копать нишу, которая ей самой интересна. Excalidraw, кстати, начался 1 января 2020 года с твита инженера Facebook о небольшом приложении для рисования.
Маленькие команды ещё и быстрее берут в работу новые инструменты. Anthropic разобрали 500 тысяч диалогов про код. По предварительной оценке, 33% диалогов в Claude Code касались работы стартапов и только 13% задач крупных компаний.
Если вы знаете нишу изнутри и пользователи вам доверяют, крупному конкуренту понадобятся годы, чтобы это догнать.
Шаг 6. Проверьте деньгами, без программиста
Пол Бакхайт, один из партнёров YC, советует командам с сомнительной идеей сначала попробовать её продать, а строить потом. Пока продаёшь, часто находится настоящая идея.
Первую проверку можно провести за выходные с лендингом, листом ожидания и предзаказом. В зачёт идут только оплаты и предзаказы.
Прототип тоже соберёт ИИ. Примерно у четверти стартапов зимнего набора YC 2025 года 95% кода написал ИИ, а весь набор рос в сумме на 10% в неделю.
Без инженера не обойтись там, где появляются пользователи и данные. Чтобы совместное редактирование Excalidraw заработало на своём сервере, нужны отдельный сервер для совместной работы и HTTPS, и настраивать это лучше инфраструктурному инженеру.
Маркетинг без бюджета
Реклама для российского рынка ограничена, а на большие бюджеты у маленькой команды нет денег. Зато можно попасть в момент или сделать так, чтобы продуктом делились сами пользователи.
Когда Miro уходил, Pruffme сделал импорт досок. Сначала через него перенесли 146 досок, а к августу 2024 года уже 12 тысяч.
Excalidraw разошёлся по Twitter за две недели после поста автора о первых результатах.
Компания из одного человека
В мае 2025 года Дарио Амодеи, CEO Anthropic, предсказал, что в 2026 году появится первая компания стоимостью миллиард долларов с одним сотрудником. Инструменты для такой компании уже есть у всех. Но в Массачусетском технологическом институте заметили, что люди, которые пишут с ChatGPT, хуже помнят свой текст и меньше считают его своим. С нишей то же самое. Если только пересылать выводы модели, рынок вы так и не узнаете и не отличите настоящий сигнал от уверенной ошибки. Нейросеть найдёт и разложит источники, а читать их, сомневаться и решать, что строить, придётся вам. Автор продукта и его первый пользователь всё равно вы.
Mentions
How Much Memory Does a CI Build Need? Five Real Builds at 2, 4 and 8 GB --- title: How Much Memory Does a CI Build Need? Five Real Builds at 2, 4 and 8 GB published: true date: 2026-11-02 18:00:00 UTC tags: CICD,Memory,cgroups,Docker canonical_url: devops-daily.com --- We ran five real open-source builds (a Vite app build, a TypeScript type check, a Gradle build, a Go build and a Docker image build) inside 2, 4 and 8 GB memory limits on one 4 vCPU machine, with Gradle both with and without its daemon, two or three times at each limit. Four of the five finished inside 2 GB. The one that did not was the Excalidraw web app: its Vite production build also runs a full TypeScript check, together they held 3,248 MiB of anonymous memory, and the kernel killed the build at 2 GB in all 3 runs. The number that most tools show you was the wrong one for the other four. cgroup v2's memory.peak said the GitHub CLI's Go build peaked at 1,775 MiB, but only 424 MiB of that was anonymous memory, the heaps and stacks of its processes. Most of the rest was page cache, which the kernel drops when it needs the room, plus some kernel memory. A Docker image build looked like 2,072 MiB for the same reason. And a TypeScript type check that held 1,139 MiB with the whole machine available finished inside 2 GB with 970 MiB, because Node sizes its heap limit from the cgroup limit it finds. This post shows the numbers, the five traps behind them, and every script we used, so you can size a runner from your own builds instead of from a guess. TL;DR - Four of five builds fit in 2 GB. Only the Excalidraw Vite build needed more: 3,248 MiB of anonymous memory, killed at 2 GB, fine at 4 GB. - memory.peak counts page cache. The Go build and the Docker build showed 1,775 and 2,072 MiB, but their anonymous memory peaked at 424 and 355 MiB. Size from anonymous memory plus a margin, then confirm by running the build inside that limit. - Runtimes size their heaps from the cgroup limit. Inside 2 GB, Node's default heap limit was 1,120 MiB and the JVM's default maximum heap was 512 MiB. The type check used less memory at 2 GB than at 8 GB and still finished, about 24% slower. - The Gradle daemon keeps its memory after the build. Five seconds after gradlew exited, the daemon still held 1,048 MiB of anonymous memory. With --no-daemon, that dropped to under 1 MiB, and in our one-command runs the daemon gave no repeatable speed-up. - Limiting the docker CLI did not limit the build. The CLI's cgroup peaked at 40 MiB while the BuildKit container that did the work peaked at 2,072 MiB. - Wall times on a shared-CPU droplet are noisy: the CPUs reported 23.5% steal time across the runs we tracked. Trust the memory columns more than the time columns. Prerequisites - A Linux machine with cgroup v2 and systemd (any recent Ubuntu, Debian or Fedora). Check with stat -fc %T /sys/fs/cgroup, which prints cgroup2fs. - Root access, for systemd-run --scope and for dropping the page cache. - The toolchains for the builds you want to test. Our setup script installs Go, Node, Yarn, a JDK, Rust and Docker. - If you want to rerun our matrix as it is: a 4 vCPU, 8 GB machine and about two hours. We used a droplet on DigitalOcean (referral link). How we measured We used one DigitalOcean droplet, size s-4vcpu-8gb in fra1, with Ubuntu 24.04 and kernel 6.8. The kernel reports 7,941 MiB of memory and there is no swap. Every build ran in its own transient systemd scope: systemd-run --scope --slice=bench.slice --unit=p1-<build>-<limit>-<run> \ -p MemoryMax=2G -p MemorySwapMax=0 -p AllowedCPUs=0-3 -p OOMPolicy=continue \ /root/bench/wrap.sh <outdir> <timeout> <build command> These are cgroup limits on one droplet, not three real machine sizes. A real 2 GB runner has a smaller kernel footprint, a runner agent and often a Docker daemon in the same memory, so it has less room than our 2 GB scope. The 8 GB limit is above the 7.9 GiB that the droplet has, so the 8 GB column means "the whole machine". The rest of the setup: - CPU. Every scope was pinned to the same 4 vCPUs with AllowedCPUs=0-3, and nproc printed 4 inside each one. Compilers start about one job per CPU, so a 2 vCPU runner would use less memory and an 8 vCPU runner more. We did not measure other CPU counts. - Cold build, warm dependencies. We downloaded dependencies once before the runs: yarn install, go mod download, cargo fetch, the Gradle distribution and dependencies, and the base images for the Docker build. Before every run we deleted the build output and the compiler caches (excalidraw-app/build, GOCACHE, target/, Gradle's build/ and .gradle/), stopped any Gradle daemon, and dropped the page cache. That is a CI job that restores a dependency cache but not a build cache. - What we recorded. Wall time, the exit code, memory.peak at the end of the run, memory.events (how often the limit was hit, and OOM kills), and a sampler that read memory.stat every 100 ms for anonymous memory (anon) and page cache (file). - Repeats. Runs went in round-robin order: run 1 of every cell, then run 2, then run 3, so a slow period on the shared host does not land on one build. Builds ran slower than we planned, so to stay inside our droplet budget the third round covered only the 8 GB and 2 GB limits. That gives 3 runs per cell at 8 GB and 2 GB, and 2 at 4 GB. The tables show the median of 3, or the mean of 2 at 4 GB. Each build also had one warm-up run at 8 GB that we did not count; it downloaded Gradle and filled the image mirror. The builds, all pinned to exact commits: | Build | Command | Source | | --- | --- | --- | | Excalidraw app, Vite production build | yarn build:app:docker | excalidraw/excalidraw 8bcdca45 | | Excalidraw type check, 1,508 files | tsc -p tsconfig.json (noEmit is set) | same checkout | | PlantUML, Gradle 9.3.1 on JDK 21 | ./gradlew jar, daemon on and --no-daemon | plantuml/plantuml db06d70e | | GitHub CLI, Go 1.27.3 | go build ./cmd/gh | cli/cli 9490d204 | | Online Boutique frontend image, BuildKit 0.34.1 | docker buildx build | GoogleCloudPlatform/microservices-demo c7cb6d5b | | ripgrep 15.2.0, Rust 1.99.1 (one run) | cargo build --release --locked | BurntSushi/ripgrep tag 15.2.0 | Node was 24.22.2 with Yarn 1.22.22, Docker was 29.9.2 with Buildx 0.38.1. The results | Build | Anonymous peak at 8 GB | memory.peak at 8 GB | 8 GB | 4 GB | 2 GB | | --- | --- | --- | --- | --- | --- | | Excalidraw vite build | 3,248 MiB | 3,578 MiB | ok, 66 s | ok, 83 s | 3 of 3 OOM-killed (run 1 from the kernel log) | | Excalidraw tsc type check | 1,139 MiB | 1,271 MiB | ok, 59 s | ok, 62 s | ok, 73 s | | PlantUML Gradle, daemon | 1,143 MiB | 1,474 MiB | ok, 124 s | ok, 110 s | ok, 113 s | | PlantUML Gradle, --no-daemon | 1,144 MiB | 1,475 MiB | ok, 88 s | ok, 119 s | ok, 100 s | | GitHub CLI go build | 424 MiB | 1,775 MiB | ok, 141 s | ok, 198 s | ok, 169 s | | Online Boutique frontend, docker buildx build | 355 MiB | 2,072 MiB | ok, 131 s | ok, 129 s | ok, 110 s | Times are medians of 3 runs at 8 GB and 2 GB, and the mean of 2 runs at 4 GB. Memory columns are from the 8 GB runs. What the cgroup counter says vs what the build held (8 GB limit, median of 3) | | Value | Series | | --- | --- | --- | | Vite build | 3578 MiB | memory.peak | | Vite build | 3248 MiB | anonymous memory | | tsc check | 1271 MiB | memory.peak | | tsc check | 1139 MiB | anonymous memory | | Gradle (daemon) | 1474 MiB | memory.peak | | Gradle (daemon) | 1143 MiB | anonymous memory | | Gradle (no daemon) | 1475 MiB | memory.peak | | Gradle (no daemon) | 1144 MiB | anonymous memory | | Go build | 1775 MiB | memory.peak | | Go build | 424 MiB | anonymous memory | | Docker build | 2072 MiB | memory.peak | | Docker build | 355 MiB | anonymous memory | _memory.peak includes page cache; anonymous memory is the sampled peak of anon in memory.stat (100 ms). One s-4vcpu-8gb droplet, 4 vCPUs, 2 November 2026._ Three things stand out. Only one build reached the 2 GB limit at all. The max counter in memory.events, which counts how often a cgroup's usage was about to go over its limit, stayed at 0 for every other run. Memory use at 4 GB and at 8 GB was almost the same for every build, so the extra 4 GB bought nothing. And the gap between memory.peak and anonymous memory is large for exactly the builds that read and write many files: the Go compiler and BuildKit. The single Rust build, ripgrep 15.2.0 with cargo build --release, fits the same pattern: 896 MiB memory.peak, 572 MiB anonymous, 61.1 s. We ran it once at 8 GB only, because the full matrix did not fit our time budget. Trap 1: memory.peak counts page cache memory.peak is the highest value of memory.current that the cgroup reached. memory.current charges the cgroup for everything it caused to be in memory, and that includes page cache : the source files the compiler read, the object files it wrote, the layers BuildKit unpacked. Page cache is reclaimable. When the cgroup gets close to its limit, the kernel drops clean cache pages and writes back dirty ones before it ever considers killing a process. Anonymous memory is the part that cannot go anywhere without swap: heaps, stacks, the compiler's working data. memory.stat reports it as anon. That, plus a smaller amount of kernel memory (37 to 136 MiB in these builds), is what has to fit. Our sampler read it every 100 ms, so it can miss a shorter spike. memory.peak and the max counter cannot miss one, and they show that no build except Vite at 2 GB ever reached its limit. The safe test is still the one we ran: the build inside the limit you plan to buy. The Go build of the GitHub CLI shows the difference well. Its peak was 1,775 MiB. Anonymous memory never went over 424 MiB; about 1,400 MiB was page cache from the module sources and the build cache that go build writes. At the 2 GB limit, the same build finished with the same peak, because 1,775 MiB is still under 2,048 MiB. With a smaller limit the kernel may reclaim that cache instead of failing the build, but we did not test below 2 GB, so test the lower limit before you rely on it. So if a dashboard shows that your CI job "peaked at 1.8 GB", that does not mean a 2 GB runner is too small. Look at anon in memory.stat (or RSS in your container metrics), and then add a margin for the runner itself. Trap 2: a production build that is also a type check The Excalidraw build needed far more memory than anything else: 3,248 MiB of anonymous memory, against 1,139 MiB for a full type check of the same code. The reason is in its Vite config: checker({ typescript: true, eslint: envVars.VITE_APP_ENABLE_ESLINT === "false" ? undefined : { lintCommand: 'eslint "./**/*.{js,ts,tsx}"' }, // ... }), vite-plugin-checker runs the TypeScript checker during vite build too: in version 0.7.2, which Excalidraw pins, enableBuild defaults to true. The production .env turns ESLint off but leaves the type check on, and the build also writes source maps. So one vite build is Rollup, source maps and a full tsc run at the same time. At 2 GB the kernel killed it in all 3 runs, 69 to 122 seconds in, after the log said ✓ 1442 modules transformed. The kernel log names the cgroup and the process it chose: oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null),cpuset=p1-vite-2G-r2.scope,mems_allowed=0,oom_memcg=/bench.slice/p1-vite-2G-r2.scope,task_memcg=/bench.slice/p1-vite-2G-r2.scope,task=MainThread,pid=77242,uid=0 Memory cgroup out of memory: Killed process 77242 (MainThread) total-vm:11451308kB, anon-rss:1138988kB, file-rss:4864kB, shmem-rss:0kB, UID:0 pgtables:29992kB oom_score_adj:0 Our wrapper's record of that run shows the kill. memory.events counted 39,825 times that the cgroup was about to go over its limit, and one OOM kill: vite build, 2 GB limit, run 2 $ cat /root/res/p1/vite/2G/r2/result.env rc=1 t0=1793637034.083018067 t1=1793637156.285208072 peak=2147483648 current_after=867889152 anon_after=801075200 file_after=38674432 $ cat /root/res/p1/vite/2G/r2/memory.events low 0 high 0 max 39825 oom 1 oom_kill 1 oom_group_kill 0 The process list right after the kill shows the second half of the build. The checker runs tsc as its own child process, and it was still running after Vite had died: 77265 1808 /bin/sh -c tsc --noEmit 77266 799108 node /root/src/excalidraw/node_modules/.bin/tsc --noEmit That tsc held 799,108 KiB (780 MiB) of resident memory under the 2 GB limit. Yarn reported Command failed with exit code 1, but the type check kept going until our harness stopped the scope. On a real runner it would keep using memory and CPU until the job ends. We did not split the 3,248 MiB peak at 8 GB between Rollup and the checker, so we cannot say exactly how much the type check adds. What we can say: if your bundler runs a checker, your build step needs the memory of both, and moving the type check to its own CI job is the first thing to try before you buy a bigger runner. Trap 3: runtimes size their heaps from the limit Node and the JVM both read the cgroup limit when they start and size their default heap from it. Our harness asks each runtime what it would use inside each limit: probe inside a 2 GB scope # what Node 24 and JDK 21 pick inside MemoryMax=2G (from p1.sh probe) $ cat /root/res/p1/probe-2G.txt nproc=4 memory.max=2147483648 node_heap_size_limit=1174405120 int ActiveProcessorCount = -1 {product} {default} size_t MaxHeapSize = 536870912 {product} {ergonomic} double MaxRAMPercentage = 25.000000 {product} {default} All four places side by side: | Inside | Node 24 heap_size_limit | JDK 21 default MaxHeapSize | | --- | --- | --- | | 2 GB scope | 1,120 MiB | 512 MiB | | 4 GB scope | 2,240 MiB | 1,024 MiB | | 8 GB scope | 2,240 MiB | 2,048 MiB | | whole droplet, no scope | 2,240 MiB | 1,986 MiB | Two consequences showed up in the runs. Heap use follows the limit. tsc --extendedDiagnostics prints Memory used, which is Node's heapUsed at the end of the run, in units of 1,000 bytes. With the whole machine, the type check of 1,508 files reported Memory used: 1018378K (median of 3, about 971 MiB). Inside 2 GB it reported 886903K (about 846 MiB), anonymous memory fell from 1,139 to 970 MiB, and the check finished about 24% slower (median 72.7 s against 58.8 s). The 2 GB run was 9.5 to 14.2 seconds slower in each of the three rounds. In rounds 2 and 3, where we logged the CPU counters, its steal time was only 4 to 7 points higher, so steal explains only part of the gap. More frequent garbage collection under the smaller heap limit would explain the rest, but we did not record GC activity, so that is a hypothesis. A big heap number at a high limit says little about what the program needs. We expected this check to fail at 2 GB with a heap error, and it did not. Node stopped at 2,240 MiB even with more memory. On our 8 GB machine, Node's default heap limit was 2,240 MiB with a 4 GB scope, an 8 GB scope and no scope at all. We did not test a bigger machine, so check what your runner gives you with node -e 'console.log(require("v8").getHeapStatistics().heap_size_limit)'. If a build reports JavaScript heap out of memory, that is Node's own limit, not the kernel's. Set NODE_OPTIONS=--max-old-space-size=<MiB> explicitly instead of only buying more memory. The JVM takes 25% of the limit by default (MaxRAMPercentage=25), so a Maven build or a test JVM without -Xmx gets 512 MiB inside 2 GB. Gradle sets its own daemon heap, which brings us to the next trap. Trap 4: the Gradle daemon outlives the build PlantUML's gradle.properties sets org.gradle.jvmargs=-Xmx1g. That cap kept the build inside 2 GB: anonymous memory peaked at about 1,143 MiB with the daemon and 1,144 MiB without it, and neither came near the limit. The difference is what happens after gradlew exits. With the daemon, which is the default, the JVM stays alive and keeps its heap. Five seconds after the build, the scope still held 1,378 MiB, 1,048 MiB of it anonymous, in one process: 37292 1105976 /usr/lib/jvm/java-21-openjdk-amd64/bin/java --add-opens=java.base/java.lang=ALL-UNNAMED ... With --no-daemon, Gradle printed To honour the JVM settings for this build a single-use Daemon process will be forked and Daemon will be stopped at the end of the build, and five seconds after exit the anonymous memory in the scope was under 1 MiB. Anonymous memory around the end of the Gradle build (8 GB limit, run 1) | | -55 s | -50 s | -45 s | -40 s | -35 s | -30 s | -25 s | -20 s | -15 s | -10 s | -5 s | 0 s | +5 s | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | daemon (default) | 637 MiB | 857 MiB | 927 MiB | 982 MiB | 1053 MiB | 1059 MiB | 1066 MiB | 1073 MiB | 1082 MiB | 1110 MiB | 1115 MiB | 1143 MiB | 1054 MiB | | --no-daemon | 476 MiB | 516 MiB | 559 MiB | 807 MiB | 921 MiB | 970 MiB | 1067 MiB | 1072 MiB | 1080 MiB | 1084 MiB | 1112 MiB | 1144 MiB | 0 MiB | _Max of 100 ms samples in each 5 s bin, labelled by the end of the bin; 0 s is the moment gradlew exits, +5 s is the 5 s after. One run each, PlantUML jar task, JDK 21, Gradle 9.3.1._ On a CI runner, that leftover daemon sits in the same memory as every later step of the job: the Docker build, the integration tests, the upload. At 8 GB the median time was 123.5 s with the daemon and 87.5 s without (112.7 s and 99.7 s at 2 GB), The ranges overlap and CPU steal was high, so we see no repeatable speed-up from the daemon for one Gradle command per job. The daemon is built to pay off across several Gradle commands in the same job, which we did not test. If a job runs one Gradle command, use --no-daemon (or org.gradle.daemon=false). If it runs several, consider keeping the daemon (we did not test that case), and run ./gradlew --stop before the steps that need the memory. Trap 5: a limit on the docker CLI does not limit the build docker build is a client. In our setup the work ran in a BuildKit container (Buildx's docker-container driver), which has its own cgroup. Other drivers and CI executors lay out cgroups differently, but the CLI is only a client in all of them. We wrapped the docker buildx build command in the same kind of scope as everything else, and at every limit that scope peaked at 39 to 42 MiB: the CLI, streaming logs. To measure the build, we put the limit on the BuildKit container itself: docker buildx create --name p1d8gr1 --driver docker-container \ --driver-opt network=host --driver-opt image=moby/buildkit:v0.34.1 \ --driver-opt memory=8g --driver-opt memory-swap=8g --driver-opt cpuset-cpus=0-3 \ --buildkitd-config buildkitd.toml --bootstrap and read memory.peak from /sys/fs/cgroup/system.slice/docker-<container id>.scope. That container peaked at 2,072 MiB, of which only 355 MiB was anonymous: the Go compiler inside the build. Most of the rest was page cache from the image layers and the build's output, plus 136 MiB of kernel memory. This matters for runner sizing in two ways. Your CI's memory graph may not show the build at all, depending on whether it measures the job's process tree or the whole machine. And if a self-hosted runner limits only the job's process tree, a Docker build in that job can run outside the limit, so a "2 GB job" can push the host far past 2 GB. Check which cgroup your executor puts the Docker daemon in. Two method bugs that cost us a run The first 2 GB Vite run left no result at all. The kernel killed the Vite process, and then systemd stopped the whole scope, including our wrapper script, before it could write anything. The journal said so: p1-vite-2G-r1.scope: A process of this unit has been killed by the OOM killer. p1-vite-2G-r1.scope: Failed with result 'oom-kill'. systemd 255 sets DefaultOOMPolicy=stop, and scopes follow it. We set DefaultOOMPolicy=continue before the next 2 GB run (the published p1.sh also passes -p OOMPolicy=continue), and checked with a 64 MiB scope that the wrapper now survives a kill and records oom_kill 1. Runs 2 and 3 at 2 GB have full records. Then our rerun of run 1 failed too, for a different reason. systemd keeps a failed transient unit loaded, so starting a new scope with the same name was refused: Failed to start transient scope unit: Unit p1-vite-2G-r1.scope was already loaded or has a fragment file. By the time we saw that, the Docker cache tests for the next post were running on the same droplet, so we did not try again. Run 1 still counts as a kill: its kernel log line reads oom_memcg=/bench.slice/p1-vite-2G-r1.scope,...,task=MainThread,pid=36041, and our sampler, which runs outside the scope, saw the cgroup reach 2,048 MiB, with up to 1,985 MiB anonymous, before its last sample at 70.5 seconds. The published redo mode now runs systemctl reset-failed first. If you measure builds this way, set OOMPolicy=continue and give each run a unique unit name, or the runs that you care about most, the ones that fail, will be the ones with no data. A sizing table From these runs, for a 4 vCPU runner. The "smallest limit that passed" column is measured. The last column is our rule of thumb, not a measurement: a real VM also holds the OS, the runner agent and any service containers, so we pick the smallest size that leaves at least 1 GB above the anonymous peak. | Build like ours | Anonymous peak | Smallest limit that passed every run | Runner we would pick | | --- | --- | --- | --- | | Bundler that also type-checks (Excalidraw vite build) | 3,248 MiB | 4 GB | 8 GB (moving the type check to its own job may make 4 GB enough; we did not test that) | | Large type check on its own (tsc, 1,508 files) | 1,139 MiB at 8 GB, 970 MiB at 2 GB | 2 GB | 4 GB | | Gradle with a 1 GB daemon heap (PlantUML jar) | 1,143 MiB | 2 GB | 4 GB, with --no-daemon | | Go build of a mid-size CLI (gh) | 424 MiB | 2 GB | 2 GB | | Docker build of a Go service | 355 MiB inside BuildKit | 2 GB | 2 GB, plus what the Docker daemon and the other steps need | Hosted CI services list their machines by vCPU and memory, so you can match a build to a size. Bitrise, for example, lists every Linux machine type on its pricing page with its vCPU count and memory, such as 2 vCPU with 8 GB, and also sells a remote build cache for Gradle, Bazel and Xcode (checked on its public pages on 2 November 2026). Whatever you use, measure anonymous memory first, then pick the size. What we could not conclude - Three limits on one machine are not three machines. A real 2 GB runner has less page cache to spare, its own kernel, and a runner agent in the same memory. Leave a margin above the anonymous peak; our sizing table uses 1 GB. - CPU count changes memory. Every run had 4 vCPUs. The Go compiler, Rollup's workers and Gradle's workers scale with CPUs, so these numbers are not valid for 2 or 16 vCPUs. - Wall times are noisy. The droplet reported 23.5% CPU steal time across the 33 runs that our CPU counter log covered, and between 3.7% and 35.5% in single runs. The Go build took between 121.3 and 199.6 seconds at limits it never came close to. We do not read anything into a time difference between limits unless it repeats in every round, as the tsc gap did. - Two or three runs per cell is enough to see a failure repeat and a peak hold steady, and that is all. The anonymous peaks varied by up to 21% for the Go build (353 to 441 MiB at 8 GB) and up to 15% for Gradle, and 6% or less for Vite, tsc and Docker between runs of the same cell. - We did not split shared builds. We cannot say how much of the Vite build's memory is Rollup and how much is the checker, or how much of the Docker build's page cache is layer extraction. - Rust ran once. One ripgrep build at 8 GB, so no repeats and no 2 GB or 4 GB data. Run it yourself Create a 4 vCPU, 8 GB Ubuntu 24.04 machine, copy the files below to /root/bench/, and run: bash /root/bench/setup.sh bash /root/bench/p1.sh warmup # probes the runtimes, one unmeasured run per build bash /root/bench/p1.sh matrix # 3 rounds x 3 limits x each build (we stopped it after round 2) bash /root/bench/p1.sh round3 # what we ran as round 3: 8 GB and 2 GB only bash /root/bench/p1.sh redo # reruns any cell that left no result python3 summarize_p1.py /root/res/p1 Each run writes its own directory with result.env, memory.events, samples.json and the build log, so you can read any single run on its own. #setup.sh: toolchains, pinned sources, dependencies, registries #!/bin/bash # Droplet setup: toolchains, pinned sources, dependency prefetch, local registries. set -euxo pipefail export DEBIAN_FRONTEND=noninteractive mkdir -p /root/src /root/res cloud-init status --wait || true # first boot runs apt; wait for its lock fetch_sha() { git init -q "$1"; git -C "$1" remote add origin "$2"; git -C "$1" fetch -q --depth 1 origin "$3"; git -C "$1" checkout -q FETCH_HEAD; } fetch_sha /root/src/excalidraw https://github.com/excalidraw/excalidraw 8bcdca4526867433f99fdfcb55a063668db0c9dc fetch_sha /root/src/plantuml https://github.com/plantuml/plantuml db06d70e18e71839dd5e330ac4086f3845371e3b fetch_sha /root/src/cli https://github.com/cli/cli 9490d204a2ba95434153dedce99898ce9f281426 git clone -q --depth 1 --branch 15.2.0 https://github.com/BurntSushi/ripgrep /root/src/ripgrep fetch_sha /root/src/msd https://github.com/GoogleCloudPlatform/microservices-demo c7cb6d5bfd6ea3aed768634ebbcb98cb7a523712 curl -fsSL https://go.dev/dl/go1.27.3.linux-amd64.tar.gz | tar -C /usr/local -xz curl -fsSL https://nodejs.org/dist/v24.22.2/node-v24.22.2-linux-x64.tar.xz | tar -C /usr/local --strip-components=1 -xJ curl -fsSL https://sh.rustup.rs | sh -s -- -y -q --profile minimal --default-toolchain stable apt-get update -q apt-get install -y -q openjdk-21-jdk-headless build-essential git jq zstd python3 ca-certificates curl curl -fsSL https://get.docker.com | sh export PATH=/usr/local/go/bin:/root/.cargo/bin:/usr/local/bin:$PATH GOTOOLCHAIN=local npm install -g yarn@1.22.22 docker pull -q moby/buildkit:v0.34.1 docker pull -q registry:2.8.3 docker run -d --name reg-cache -p 127.0.0.1:5000:5000 registry:2.8.3 docker run -d --name mirror-hub -p 127.0.0.1:5001:5000 -e REGISTRY_PROXY_REMOTEURL=https://registry-1.docker.io registry:2.8.3 docker run -d --name mirror-gcr -p 127.0.0.1:5002:5000 -e REGISTRY_PROXY_REMOTEURL=https://gcr.io registry:2.8.3 # dependencies are downloaded once, outside the measured runs (cd /root/src/excalidraw && yarn install --frozen-lockfile --network-timeout 600000) (cd /root/src/cli && go mod download) (cd /root/src/ripgrep && cargo fetch --locked) # systemd stops a scope after an OOM kill by default; keep the wrapper alive to record the result mkdir -p /etc/systemd/system.conf.d printf '[Manager]\nDefaultOOMPolicy=continue\n' > /etc/systemd/system.conf.d/oom.conf systemctl daemon-reexec #wrap.sh: runs inside the scope and records the result #!/bin/bash # Runs inside the systemd scope. usage: wrap.sh OUTDIR TIMEOUT_S cmd... out=$1; tmo=$2; shift 2 cg=/sys/fs/cgroup$(sed -n 's/^0:://p' /proc/self/cgroup) echo "$cg" > "$out/cgroup" cat "$cg/memory.max" > "$out/memory.max" t0=$(date +%s.%N) timeout -k 15 "$tmo" "$@" > "$out/build.log" 2>&1 rc=$? t1=$(date +%s.%N) { echo "rc=$rc" echo "t0=$t0" echo "t1=$t1" echo "peak=$(cat "$cg/memory.peak")" echo "current_after=$(cat "$cg/memory.current")" awk '$1=="anon"||$1=="file"{print $1"_after="$2}' "$cg/memory.stat" } > "$out/result.env" cp "$cg/memory.events" "$out/memory.events" for p in $(cat "$cg/cgroup.procs"); do ["$p" = "$$"] && continue ps -o pid=,rss=,args= -p "$p" | cut -c1-400 done > "$out/leftover.txt" 2>/dev/null if [-n "${LEFTOVER_WAIT:-}"]; then sleep "$LEFTOVER_WAIT" echo "current_later=$(cat "$cg/memory.current")" >> "$out/result.env" awk '$1=="anon"{print "anon_later="$2}' "$cg/memory.stat" >> "$out/result.env" for p in $(cat "$cg/cgroup.procs"); do ["$p" = "$$"] && continue ps -o pid=,rss=,args= -p "$p" | cut -c1-400 done > "$out/leftover_later.txt" 2>/dev/null fi touch "$out/done" exit $rc #sample.py: polls memory.stat every 100 ms from outside the scope #!/usr/bin/env python3 """Poll one cgroup v2 directory every 100 ms until a done file appears. usage: sample.py CGROUP_DIR OUT_JSON DONE_FILE Records the maximum of anon, file, kernel and memory.current, plus a series. """ import json, os, sys, time cg, out, done = sys.argv[1:4] start = time.time() while not os.path.isdir(cg): if time.time() - start > 60 or os.path.exists(done): json.dump({"error": "cgroup never appeared"}, open(out, "w")) sys.exit(0) time.sleep(0.02) keys = ("anon", "file", "kernel", "shmem") mx = {k: 0 for k in keys} mx["current"] = 0 series = [] t0 = time.time() n = 0 while not os.path.exists(done): try: with open(os.path.join(cg, "memory.stat")) as f: st = {} for line in f: k, v = line.split() if k in keys: st[k] = int(v) with open(os.path.join(cg, "memory.current")) as f: cur = int(f.read()) except OSError: break n += 1 for k in keys: mx[k] = max(mx[k], st.get(k, 0)) mx["current"] = max(mx["current"], cur) series.append([round(time.time() - t0, 2), st.get("anon", 0), st.get("file", 0), cur]) time.sleep(0.1) json.dump({"max": mx, "samples": n, "interval_s": 0.1, "series": series}, open(out, "w")) #p1-cmds.sh: the builds, and what is deleted before each run #!/bin/bash # Build definitions for post 1. prep_* runs outside the cgroup, cmd_* inside it. export PATH=/usr/local/go/bin:/root/.cargo/bin:/usr/local/bin:$PATH GOTOOLCHAIN=local EX=/root/src/excalidraw PU=/root/src/plantuml GH=/root/src/cli RG=/root/src/ripgrep FRONTEND=/root/src/msd/src/frontend stop_gradle() { pkill -f GradleDaemon || true; pkill -f GradleWrapperMain || true; pkill -f KotlinCompileDaemon || true; sleep 1; } prep_vite() { rm -rf $EX/excalidraw-app/build $EX/node_modules/.vite $EX/excalidraw-app/node_modules/.vite; } cmd_vite() { cd $EX && yarn build:app:docker; } prep_tsc() { find $EX -maxdepth 2 -name '*.tsbuildinfo' -delete; } cmd_tsc() { cd $EX && node_modules/.bin/tsc -p tsconfig.json --extendedDiagnostics; } prep_gradle_daemon() { stop_gradle; rm -rf $PU/build $PU/.gradle; } cmd_gradle_daemon() { cd $PU && ./gradlew jar --console=plain --no-build-cache; } prep_gradle_nodaemon() { stop_gradle; rm -rf $PU/build $PU/.gradle; } cmd_gradle_nodaemon() { cd $PU && ./gradlew --no-daemon jar --console=plain --no-build-cache; } prep_go() { rm -rf /root/gocache /root/out; mkdir -p /root/out; } cmd_go() { cd $GH && GOCACHE=/root/gocache go build -o /root/out/gh ./cmd/gh; } prep_rust() { rm -rf $RG/target; } cmd_rust() { cd $RG && cargo build --release --locked; } prep_docker() { docker buildx ls --format '{{.Name}}' 2>/dev/null | grep '^p1d' | xargs -r -n1 docker buildx rm >/dev/null 2>&1 || true; } if ["${BASH_SOURCE[0]}" = "$0" ]; then "$@"; fi #p1.sh: probe, warm-up, matrix, round 3, redo, and the docker build cell #!/bin/bash # Post 1 matrix. usage: p1.sh warmup | p1.sh matrix set -u B=/root/bench R=/root/res/p1 mkdir -p $R source $B/p1-cmds.sh LIMITS=(8G 4G 2G) BUILDS=(vite tsc gradle_daemon gradle_nodaemon go rust docker) # rust ran once (warmup) only: the matrix did not fit the droplet time budget MATRIX_BUILDS=(vite tsc gradle_daemon gradle_nodaemon go docker) CPUS=0-3 log() { echo "$(date -u +%FT%TZ) $*" | tee -a $R/progress.log; } scope_run() { # unit limit out tmo cmd... local unit=$1 lim=$2 out=$3 tmo=$4; shift 4 python3 $B/sample.py /sys/fs/cgroup/bench.slice/$unit.scope $out/samples.json $out/done & local sp=$! systemd-run --scope --quiet --slice=bench.slice --unit=$unit \ -p MemoryMax=$lim -p MemorySwapMax=0 -p AllowedCPUs=$CPUS -p OOMPolicy=continue \ $B/wrap.sh $out $tmo "$@" wait $sp systemctl stop $unit.scope 2>/dev/null || true } docker_cell() { # lim out tmo tag local lim=$1 out=$2 tmo=$3 tag=$4 local name=$(echo p1d$tag | tr A-Z a-z) m=$(echo $1 | tr G g) docker buildx create --name $name --driver docker-container \ --driver-opt network=host --driver-opt image=moby/buildkit:v0.34.1 \ --driver-opt memory=$m --driver-opt memory-swap=$m --driver-opt cpuset-cpus=$CPUS \ --buildkitd-config $B/buildkitd.toml --bootstrap > $out/create.log 2>&1 local cid; cid=$(docker inspect -f '{{.Id}}' buildx_buildkit_${name}0) local cg=/sys/fs/cgroup/system.slice/docker-$cid.scope cat $cg/memory.max > $out/container_memory.max mkdir -p $out/cli python3 $B/sample.py $cg $out/container_samples.json $out/cli/done & local sp=$! scope_run p1-docker-$tag $lim $out/cli $tmo \ docker buildx build --builder $name --progress=plain --provenance=false --output type=cacheonly $FRONTEND wait $sp { echo "container_peak=$(cat $cg/memory.peak)"; cat $out/cli/result.env; } > $out/result.env cp $cg/memory.events $out/container_memory.events cp $out/cli/build.log $out/build.log docker buildx rm $name > /dev/null 2>&1 } run_cell() { # build limit run tmo local b=$1 lim=$2 r=$3 tmo=$4 local tag=$b-$lim-$r out=$R/$b/$lim/$r rm -rf $out; mkdir -p $out prep_$b sync; echo 3 > /proc/sys/vm/drop_caches; sleep 2 if [$b = docker]; then docker_cell $lim $out $tmo ${lim}$r else case $b in gradle_*) export LEFTOVER_WAIT=5 ;; esac scope_run p1-$tag $lim $out $tmo bash $B/p1-cmds.sh cmd_$b unset LEFTOVER_WAIT fi [$b = gradle_daemon] || [$b = gradle_nodaemon] && stop_gradle log "$tag $(tr '\n' ' ' < $out/result.env)" } probe() { # record what the runtimes see inside each limit for lim in "${LIMITS[@]}"; do systemd-run --scope --quiet --slice=bench.slice --unit=probe-$lim -p MemoryMax=$lim -p MemorySwapMax=0 -p AllowedCPUs=$CPUS \ bash -c 'echo "nproc=$(nproc)"; echo "memory.max=$(cat /sys/fs/cgroup$(sed -n "s/^0:://p" /proc/self/cgroup)/memory.max)"; echo "node_heap_size_limit=$(node -e "console.log(require(\"v8\").getHeapStatistics().heap_size_limit)")"; java -XX:+PrintFlagsFinal -version 2>/dev/null | grep -E " (MaxHeapSize|MaxRAMPercentage|ActiveProcessorCount) "' > $R/probe-$lim.txt 2>&1 done { echo "nproc=$(nproc)"; free -b | head -2; echo "node_heap_size_limit=$(node -e 'console.log(require("v8").getHeapStatistics().heap_size_limit)')"; java -XX:+PrintFlagsFinal -version 2>/dev/null | grep -E " (MaxHeapSize|MaxRAMPercentage) "; } > $R/probe-host.txt 2>&1 } case ${1:-} in warmup) probe for b in "${BUILDS[@]}"; do run_cell $b 8G r0 900; done log WARMUP-DONE ;; matrix) declare -A TMO for b in "${MATRIX_BUILDS[@]}"; do . <(grep -E '^t[01]=' $R/$b/8G/r0/result.env) w=$(python3 -c "print(int(float('$t1')-float('$t0')))") t=$((3 * w + 60)); [$t -gt 600] && t=600; [$t -lt 120] && t=120 TMO[$b]=$t done log "timeouts: $(for b in "${MATRIX_BUILDS[@]}"; do printf '%s=%s ' $b ${TMO[$b]}; done)" for r in r1 r2 r3; do for b in "${MATRIX_BUILDS[@]}"; do for lim in "${LIMITS[@]}"; do run_cell $b $lim $r ${TMO[$b]}; done done done log MATRIX-DONE ;; round3) # third run at 8G and 2G only: the full round did not fit the droplet time budget declare -A TMO=([vite]=300 [tsc]=381 [gradle_daemon]=600 [gradle_nodaemon]=351 [go]=414 [docker]=351) log "round 3 at 8G and 2G only" for b in "${MATRIX_BUILDS[@]}"; do for lim in 8G 2G; do run_cell $b $lim r3 ${TMO[$b]}; done done log MATRIX-DONE ;; redo) # rerun cells that left no result (systemd stopped the scope after an OOM kill) declare -A TMO=([vite]=300 [tsc]=381 [gradle_daemon]=600 [gradle_nodaemon]=351 [go]=414 [docker]=351) for b in "${MATRIX_BUILDS[@]}"; do for lim in "${LIMITS[@]}"; do for r in r1 r2 r3; do d=$R/$b/$lim/$r if [-d $d] && ! grep -q '^rc=' $d/result.env 2>/dev/null; then rm -rf $d.lost; mv $d $d.lost; log "redo $b $lim $r" systemctl reset-failed p1-$b-$lim-$r.scope 2>/dev/null # added after our run: a failed scope blocks its name run_cell $b $lim $r ${TMO[$b]} fi done; done; done log REDO-DONE ;; *) echo "usage: $0 warmup|matrix|round3|redo"; exit 2 ;; esac #buildkitd.toml: send base image pulls to the local mirror [registry."docker.io"] mirrors = ["127.0.0.1:5001"] [registry."gcr.io"] mirrors = ["127.0.0.1:5002"] [registry."127.0.0.1:5000"] http = true insecure = true [registry."127.0.0.1:5001"] http = true insecure = true [registry."127.0.0.1:5002"] http = true insecure = true #summarize_p1.py: medians per build and limit #!/usr/bin/env python3 """Summarize post 1 runs: medians per build and limit over the measured runs (r1..r3).""" import json, os, re, statistics, sys root = sys.argv[1] BUILDS = ["vite", "tsc", "gradle_daemon", "gradle_nodaemon", "go", "rust", "docker"] LIMITS = ["8G", "4G", "2G"] MiB = 2 ** 20 def env(path): d = {} if os.path.exists(path): for line in open(path): if "=" in line: k, v = line.strip().split("=", 1) d[k] = v return d def events(path): d = {} if os.path.exists(path): for line in open(path): k, v = line.split() d[k] = int(v) return d def run_info(d, build): r = env(os.path.join(d, "result.env")) if not r or "rc" not in r: return None log = open(os.path.join(d, "build.log"), errors="replace").read() if os.path.exists(os.path.join(d, "build.log")) else "" if build == "docker": if not r.get("container_peak"): return None # warm-up run 0 recorded no container numbers (builder name case bug) ev = events(os.path.join(d, "container_memory.events")) smp = json.load(open(os.path.join(d, "container_samples.json"))) peak = int(r["container_peak"]) cli_peak = int(r["peak"]) else: ev = events(os.path.join(d, "memory.events")) smp = json.load(open(os.path.join(d, "samples.json"))) peak = int(r["peak"]) cli_peak = None rc = int(r["rc"]) wall = float(r["t1"]) - float(r["t0"]) if rc == 0: outcome = "ok" elif ev.get("oom_kill", 0) > 0: outcome = "oom_kill" elif "heap out of memory" in log or "Reached heap limit" in log: outcome = "node_heap" elif "OutOfMemoryError" in log: outcome = "java_heap" elif rc in (124, 137): outcome = "timeout" else: outcome = f"fail_rc{rc}" mx = smp.get("max", {}) return { "rc": rc, "outcome": outcome, "wall_s": round(wall, 1), "peak_mib": round(peak / MiB), "anon_max_mib": round(mx.get("anon", 0) / MiB), "file_max_mib": round(mx.get("file", 0) / MiB), "kernel_max_mib": round(mx.get("kernel", 0) / MiB), "samples": smp.get("samples"), "oom_kill": ev.get("oom_kill", 0), "max_events": ev.get("max", 0), "high_events": ev.get("high", 0), "after_mib": round(int(r.get("current_after", 0)) / MiB), "anon_after_mib": round(int(r.get("anon_after", 0)) / MiB), "cli_peak_mib": round(cli_peak / MiB) if cli_peak is not None else None, "later_mib": round(int(r["current_later"]) / MiB) if "current_later" in r else None, "anon_later_mib": round(int(r["anon_later"]) / MiB) if "anon_later" in r else None, "leftover_later": open(os.path.join(d, "leftover_later.txt")).read().strip() if os.path.exists(os.path.join(d, "leftover_later.txt")) else None, "leftover": open(os.path.join(d, "leftover.txt")).read().strip() if os.path.exists(os.path.join(d, "leftover.txt")) else "", } def med(xs): xs = [x for x in xs if x is not None] return round(statistics.median(xs), 1) if xs else None out = {} for b in BUILDS: for lim in LIMITS: runs = {} for rn in ("r0", "r1", "r2", "r3"): d = os.path.join(root, b, lim, rn) if os.path.isdir(d): info = run_info(d, b) if info: runs[rn] = info meas = [runs[k] for k in ("r1", "r2", "r3") if k in runs] ok = [x for x in meas if x["outcome"] == "ok"] out[f"{b}/{lim}"] = { "runs": runs, "n": len(meas), "n_ok": len(ok), "outcomes": [x["outcome"] for x in meas], "wall_med_ok": med([x["wall_s"] for x in ok]), "wall_med_all": med([x["wall_s"] for x in meas]), "peak_med": med([x["peak_mib"] for x in meas]), "anon_med": med([x["anon_max_mib"] for x in meas]), "file_med": med([x["file_max_mib"] for x in meas]), "peak_range": [min(x["peak_mib"] for x in meas), max(x["peak_mib"] for x in meas)] if meas else None, "anon_range": [min(x["anon_max_mib"] for x in meas), max(x["anon_max_mib"] for x in meas)] if meas else None, "wall_range": [min(x["wall_s"] for x in meas), max(x["wall_s"] for x in meas)] if meas else None, "after_med": med([x["after_mib"] for x in meas]), "anon_after_med": med([x["anon_after_mib"] for x in meas]), "cli_peak_med": med([x["cli_peak_mib"] for x in meas]), "later_med": med([x["later_mib"] for x in meas]), "anon_later_med": med([x["anon_later_mib"] for x in meas]), "max_events_med": med([x["max_events"] for x in meas]), } json.dump(out, open(os.path.join(root, "summary.json"), "w"), indent=1) print(f"{'cell':26} {'n':>2} {'ok':>2} {'wall_ok':>8} {'peak':>6} {'anon':>6} {'file':>6} {'after':>6} outcomes / ranges") for k, v in out.items(): print(f"{k:26} {v['n']:>2} {v['n_ok']:>2} {str(v['wall_med_ok']):>8} {str(v['peak_med']):>6} {str(v['anon_med']):>6} {str(v['file_med']):>6} {str(v['after_med']):>6} {v['outcomes']} wall{v['wall_range']} anon{v['anon_range']} peak{v['peak_range']} maxev={v['max_events_med']} cli={v['cli_peak_med']} later={v['later_med']}/{v['anon_later_med']}") Summary Most CI builds need less memory than the cgroup counter suggests, because memory.peak includes page cache that the kernel gives back under pressure. Measure anonymous memory, run the build inside the limit you want to buy, and check memory.events for max and oom_kill. Then look for the traps: a bundler that runs a type checker, a runtime that sizes its heap from the limit, a Gradle daemon that stays after the build, and a Docker build that runs outside your cgroup. In our runs, fixing any of those was worth more than doubling the runner's memory. _Originally published at devops-daily.com._
GitHub: excalidraw/excalidraw Save this for your next brainstorming session. Follow ByteBuilders for more useful developer tools and open-source discoveries. #Excalidraw #OpenSource #DeveloperTools #SoftwareEngineering #Productivity #ByteBuilders
做汇报、讲方案、画流程图的打工人,这个免费神器一定要知道: Excalidraw,打开网页就能画: ① 手绘风格,随手几笔就很好看 ② 支持多人实时协作,开会边讲边画 ③ 完全免费开源,不用注册 🔗 excalidraw/excalidraw GitHub 总星 134,870 比PPT直观多了,「建议收藏」 t.co
30 projetos GRÁTIS do GitHub para trabalhar melhor com IA Descubra 30 projetos gratuitos do GitHub para usar IA, criar conteúdo e organizar seu trabalho. Veja ferramentas de vídeo, produtividade, segurança, marketing e servidores, com os links organizados por assunto abaixo. Conheça o Bora Criar com IA: boracriarcomia.com.br CAPÍTULOS 00:00 Apresentação e inteligência artificial 00:35 Segurança 00:47 Vídeo e conteúdo 01:10 Negócios e marketing 01:30 Produtividade 01:46 Servidores e infraestrutura 02:23 Projetos para explorar 02:38 Antes de instalar INTELIGÊNCIA ARTIFICIAL 1. Dify — langgenius/dify 2. Taskosaur — Taskosaur/Taskosaur 3. OpenWork — different-ai/openwork 4. Agent Browser — vercel-labs/agent-browser 5. Emdash — generalaction/emdash SEGURANÇA 6. PasteGuard — sgasser/pasteguard 7. Claude Code Safety Net — kenryu42/claude-code-safety-net VÍDEO E CONTEÚDO 8. OpenReel — Augani/openreel-video 9. BridgeClip — bridge-mind/bridgeclip 10. Recordly — webadderall/Recordly 11. Textream — f/textream 12. FFmpeg Skill — kajisho5/ffmpeg-skill 13. Presenton — presenton/presenton NEGÓCIOS E MARKETING 14. Deskcomm CRM — melgarafael/DeskcommCRM 15. Open SEO — every-app/open-seo 16. Cogsend — deepakness/cogsend PRODUTIVIDADE 17. Handy — cjpais/Handy 18. Kept — ericerkz/kept 19. Excalidraw MCP — excalidraw/excalidraw-mcp 20. ezBookkeeping — mayswind/ezbookkeeping SERVIDORES E INFRAESTRUTURA 21. Dockge — louislam/dockge 22. OpenObserve — openobserve/openobserve 23. LogDeck — AmoabaKelvin/logdeck 24. WebSSH — bifrost0x/webssh 25. Breakscale — xevrion/breakscale 26. SQLit — Maxteabag/sqlit 27. Mailflare — hieunc229/mailflare 28. Open Archiver — LogicLabs-OU/OpenArchiver PROJETOS PARA EXPLORAR 29. Vibecraft — Nearcyan/vibecraft 30. God's Eye View — bilawalsidhu/gods-eye-view Confira licença, requisitos, custos de hospedagem e manutenção de cada projeto antes de instalar. “Gratuito” pode depender de serviços externos. Referência de pauta: Hallen Tech — youtube.com Qual ferramenta você vai testar primeiro? Deixe nos comentários e inscreva-se para conhecer mais recursos de IA. #InteligenciaArtificial #GitHub #OpenSource
AI 모션그래픽 저장소 20개 모음 모션그래픽 제작에 쓰는 저장소 20개를 기획·에셋·움직임·입체 장면·출력 순서로 정리했습니다. 필요한 도구만 조합하고 각 저장소의 설치 안내와 이용 조건을 확인하면 됩니다. 도구·자료 주소 penpot/penpot excalidraw/excalidraw fontsource/fontsource lucide-icons/lucide svg/svgo lovell/sharp airbnb/lottie-web rive-app/rive-wasm motiondivision/motion greensock/GSAP juliangarnier/anime theatre-js/theatre mrdoob/three.js pmndrs/react-three-fiber pmndrs/drei pmndrs/postprocessing remotion-dev/remotion remotion-dev/skills FFmpeg/FFmpeg Vanilagy/mediabunny #모션그래픽 #개발도구 #AI활용 #투더제이 [en-US] 20 AI Motion Graphics Resources Collection We have organized 20 repositories used for motion graphics production in the order of Planning, Assets, Motion, 3D Scenes, and Output. Simply combine the tools you need and check the installation instructions and terms of use for each repository. Tool/Resource Addresses penpot/penpot excalidraw/excalidraw fontsource/fontsource lucide-icons/lucide svg/svgo lovell/sharp airbnb/lottie-web rive-app/rive-wasm motiondivision/motion greensock/GSAP gi thub.com/juliangarnier/anime theatre-js/theatre mrdoob/three.js pmndrs/react-three-fiber pmndrs/drei pmndrs/postprocessing remotion-dev/remotion remotion-dev/skills FFmpeg/FFmpeg Vanilagy/mediabunny #MotionGraphics #DevelopmentTools #AIUtilization #ToTheJ
Ищем нишу для продукта с нейросетями на примере Excalidraw Осенью 2024 года Miro отключил платные аккаунты из России и Беларуси, бесплатные оставил. За два года на освободившееся место пришли Яндекс, VK, МТС, Битрикс24 и ещё с десяток российских сервисов. Поэтому, когда в октябре 2026-го на Хабре вышла новость о новых ограничениях Miro, её почти никто не стал обсуждать: пользователи к тому времени уже переехали. Когда крупный сервис уходит с рынка, хочется сразу сделать ему замену. Но об уходе узнают все в один день, Яндекс тоже. Если делать просто ещё одну доску вместо Miro, придётся тягаться с корпорациями по бюджету, и шансов тут мало. Мне стало интересно, что получится, если копать глубже. За вечер с ИИ я нашла сегмент, до которого у корпораций не доходят руки. Это команды, которые держат open source доску Excalidraw на своих серверах, любят её и при этом скучают по функциям Miro. Статья для тех, кто пишет код и думает о своём продукте. Расскажу, какими нейросетями искать нишу, как разбирать источники и по каким фреймворкам оценивать идею, чтобы понять, принесёт ли она деньги, до того как вы потратите на неё полгода. Подход я собрала из практик Y Combinator, исследований McKinsey и Anthropic и проверила на примере с Excalidraw. Почему не стоит брать идею из подборки Пол Грэм, сооснователь Y Combinator, называет идеи, придуманные за столом, «ситкомными». В сериале они звучали бы правдоподобно, а настоящей потребности за ними нет. В его эссе есть пример с соцсетью для владельцев питомцев. Знакомые собачники вежливо скажут «может, и попробую», и на этом всё, пользователей не будет. Подборки «идей для бизнеса» почти целиком состоят из таких идей. Их прочитали тысячи людей, а спрос никто не проверял. В Y Combinator для самых заманчивых ловушек даже есть термин tarpit ideas, идеи-трясины. Они кажутся простыми, на них слетается много основателей, и годами ничего не взлетает, потому что сложность спрятана в устройстве рынка. С ИИ собрать продукт стало проще, поэтому теперь всё решает выбор, что именно собирать. В марте 2026 года Miro купил Reforge, платформу для обучения продуктовых команд. Её CEO Брайан Балфур объяснил сделку тем, что с ИИ командам нужны новые навыки, от прототипирования до стратегии. В России ошибка стоит дороже. За 2025 год российские стартапы привлекли всего $159 млн, а сделок стало на 42% меньше. Так что проверять идею приходится на свои деньги и до того, как вкладываться всерьёз. Что нейросети изменили в исследовании рынка Раньше на исследование ниши уходили недели, иностранные источники мешал читать язык, а разбор данных стоил денег. Теперь большую часть этой работы берёт на себя нейросеть. Агент глубокого исследования за время от 2 до 30 минут проходит десятки и сотни страниц и выдаёт отчёт со ссылками. Китайский, английский и русский модель читает одинаково. Поэтому Xiaohongshu (что-то вроде Pinterest и Instagram в одном) и Zhihu (китайская Quora) становятся доступны без переводчика. Тысячи отзывов модель сама раскладывает по темам, тональности и силе сигнала. Раньше на это сажали отдел аналитики. В McKinsey проверили это на продакт-менеджерах. Часть сэкономленного времени пришлась как раз на сведение пользовательских исследований, когда команда ещё ищет идею. Какие нейросети и инструменты брать Версии моделей обновляются каждые пару месяцев, так что в таблице семейства моделей и задачи, для которых я их беру в октябре 2026 года. Задача Инструмент Почему он Быстро найти источники Perplexity Deep Research Отчёт за 2-4 минуты, ссылка почти у каждого утверждения Большой структурный отчёт ChatGPT Deep Research Работает до 30 минут, отчёт выходит самый длинный и подробный Максимум источников Gemini Deep Research Просматривает 100 страниц и больше за один запрос Свести противоречивые данные в вывод Claude Research Главный агент планирует, подагенты ищут параллельно, а ссылки проверяет отдельный проход Соцсети в реальном времени Grok Лучше других работает с данными из соцсетей Русскоязычный контекст и данные внутри РФ Алиса AI и YandexGPT в Yandex Cloud, GigaChat Знают российские реалии. Сбер выложил веса крупных моделей GigaChat под лицензией MIT, их можно развернуть на своём сервере Китайские источники и дешёвая массовая обработка DeepSeek, Qwen Понимают китайский, миллион токенов стоит несколько рублей Сбор данных со страниц Apify, Firecrawl, парсеры на Python Выгружают отзывы, треды и карточки товаров в таблицу Группировка тысяч отзывов BERTopic Тематическое моделирование на эмбеддингах Одного инструмента мне обычно мало. Источники я ищу в Perplexity, а разбирать их и писать выводы отдаю Claude или ChatGPT. Такую связку советуют и в обзорах инструментов. Как обрабатывать источники Выгрузите отзывы, треды и issues в таблицу парсером или агентом. В таблице нужны текст, оценка, дата, источник и ссылка. Уберите дубли, рекламу и отзывы без текста. Если источники на разных языках, переведите всё на один. Разбейте отзывы на отдельные мнения. В одном отзыве часто несколько жалоб, и модель вытащит каждую с темой, тональностью и цитатой. Так же делают в научных работах по анализу отзывов. Сгруппируйте мнения. Их переводят в эмбеддинги и кластеризуют, обычно связкой UMAP, HDBSCAN и BERTopic. Когда методы сравнивали на отзывах, самые осмысленные темы получились у BERTopic. После этого модель может дать каждой теме короткое понятное название. Для каждой темы определите силу сигнала по таблице из шага 3, посчитайте, как часто она встречается, и посмотрите, растёт ли она со временем. Откройте несколько исходных отзывов по каждой крупной теме. Модель любит уверенно обобщать и может найти тренд, которого нет. Если отзывов несколько сотен, разбивку, группировку и оценку можно сделать одним промптом в чате. Для тысяч понадобится скрипт, но его тоже можно попросить у ИИ. Я начинала с такого промпта: Вот 300 отзывов на 1-3 звезды о сервисах онлайн-досок. Для каждого выдели отдельные жалобы. Сгруппируй жалобы в темы. Для каждой темы дай название, число упоминаний, две короткие цитаты и пометку, пишет ли кто-то, что платит за обходное решение. Отсортируй по числу упоминаний. Не добавляй тем, которых нет в отзывах. Шаг 1. Пропустите идею через 10 вопросов YC Джаред Фридман, партнёр Y Combinator, собрал эти вопросы, когда разбирал, откуда взялись идеи у сотни лучших компаний YC. Оказалось, что минимум 70% из них пришли к идее сами, из собственного опыта, без всякого мозгового штурма. Подходите ли вы для этого рынка? Фридман считает этот вопрос главным. Насколько большой рынок сейчас и будет ли он расти? Насколько острая проблема? Лучший случай, когда людям сейчас нечем её решить. Есть ли конкуренты? Чаще это хороший знак, значит, спрос есть. Хотите ли вы этот продукт сами? Знаете ли лично тех, кто хочет? Стало ли это возможным или нужным недавно? Есть ли похожие бизнесы, которые уже зарабатывают? Готовы ли вы работать над этим годами? Масштабируется ли бизнес? Насколько удачна сама область идей? Ещё Фридман пишет, что некоторые признаки только выглядят минусами. Если идею трудно начать, область кажется скучной или конкуренты уже есть, это скорее говорит в её пользу. У Грэма есть ещё один критерий, про форму спроса. По его мнению, лучше сделать продукт, который очень нужен небольшой группе, чем продукт, который всем нужен чуть-чуть. Он сравнивает такой рынок с колодцем: узко, зато глубоко. С нейросетями хорошо сочетаются ещё несколько методов. Jobs to Be Done ищет задачу, ради которой человек «нанимает» продукт. Модель неплохо достаёт такие задачи из отзывов, если попросить её писать по шаблону «когда я..., я хочу..., чтобы...». The Mom Test Роба Фитцпатрика учит спрашивать на интервью о прошлом поведении и потраченных деньгах, потому что мнение о вашей идее ничего не стоит. ИИ поможет составить вопросы и разобрать расшифровки. Дерево возможностей Терезы Торрес (Opportunity Solution Tree) идёт от цели к возможностям и решениям и держится на регулярных разговорах с пользователями. Темы, которые нашла нейросеть, ложатся в ветки этого дерева. Шаг 2. Найдите сигналы спроса Спрос видно по жалобам, обходным путям, ручной работе и деньгам, которые люди уже платят. Вот где я ищу эти следы. Больше всего, по-моему, недооценивают отзывы о конкурентах. Читать стоит отзывы на 1-3 звезды, там люди сами пишут, чего им не хватает и за что они готовы платить. Из международных площадок это G2, Capterra, Trustpilot, Product Hunt, App Store и Google Play. На китайском Xiaohongshu каждый популярный пост читается как отзыв или гайд по покупке. На Zhihu видно, какими словами люди описывают проблему и каких конкурентов сравнивают. Есть ещё отзывы на Taobao и Tmall. Аудитория там огромная, а язык ИИ переведёт. Из российских подойдут RuStore, Wildberries и Ozon, Отзовик, IRecommend, Яндекс Карты для офлайн-бизнесов и вопросы на Хабр Q&A. Я ищу повторы. Если одна и та же жалоба встречается раз двадцать, в продукте дыра. Разработчики жалуются в GitHub issues. Смотрите те, что открыты годами и собрали много реакций. В Show HN на Hacker News по комментариям видно, что людей цепляет и что раздражает, а в Ask HN прямо спрашивают, каким инструментом решить задачу. На Reddit ищите фразы «I wish there was an app for» и «alternative to». Западные тренды доходят до нас с задержкой, и это даёт фору по времени. Отдельная история, когда платформа меняет правила и пользователи остаются без инструментов. В июле 2025 года Chrome отключил расширения на Manifest V2 у всех пользователей, а с Chrome 139 они перестали работать совсем. У каждого такого расширения осталась аудитория. Можно зайти и с другой стороны и делать плагины и интеграции для растущих платформ, куда пользователи приходят быстрее, чем появляются инструменты. Популярные таблицы и шаблоны на Etsy, Gumroad и в Notion показывают, что люди до сих пор делают руками. Easlo, например, продаёт шаблоны для Notion, и к 2024 году у него набрались десятки тысяч покупателей. Шаблон, который хорошо продаётся, часто оказывается прототипом будущего продукта. Самый сильный сигнал даёт краудфандинг, потому что там люди платят за то, чего ещё нет. Kickstarter удобно смотреть как витрину спроса. Российская planeta.ru за год собрала 201 млн рублей, в основном на музыку и социальные проекты. На площадках вроде Microns продают небольшие готовые продукты вместе с пользователями и выручкой. А Y Combinator сам публикует темы, в которых видит спрос, в разделе Requests for Startups. Соцсети показывают, чем люди увлечены прямо сейчас. Если договориться с креатором о партнёрстве, дистрибуция будет с первого дня, потому что его аудитория станет вашей. В России то же самое происходит в VK Клипах и Telegram-каналах. Ещё один путь называют геоарбитражем. Продукт, который работает на одном рынке, переносят на другой и адаптируют под местные платежи и законы, у нас это СБП, 152-ФЗ и интеграция с 1С. В YC такой ориентир называют прокси. Так Rappi взял модель DoorDash и перенёс доставку еды в Латинскую Америку. Ушедшие сервисы тоже сюда относятся, просто этот сигнал видят все, и копать приходится глубже. И последнее. Если задачу решают только длинным чек-листом шагов на сорок, нормального инструмента для неё нет. Шаг 3. Оцените, будут ли платить Сигналы бывают разной силы. В таблице они идут от сильных к слабым. Сила Сигнал Что он значит 1 Предзаказ или оплата Спрос подтверждён деньгами 2 Люди платят за решение у конкурента или за хостинг open source Рынок есть, цена понятна 3 Обходной путь или ручная работа Проблема болит настолько, что на неё тратят время 4 Повторяющиеся жалобы в отзывах Проблема есть, готовность платить не проверена 5 Лайки и «было бы круто» Почти ничего не значит Потом я считаю на салфетке, сколько людей в сегменте, сколько они платят за ближайшую альтернативу и какую долю можно реально забрать за год. Если даже на зарплату одного человека не набирается, стоит искать сегмент глубже или соседний. В B2B считают так же, только сигналы тише, а цикл сделки длиннее. Решение там принимает несколько человек: пользователь, руководитель, безопасность и закупка. Поэтому искать сигнал стоит у тех, кто работает с инструментом каждый день, и сразу проверять, есть ли у них бюджет и право голоса. Самые честные сигналы в B2B лежат в открытом доступе. По вакансиям конкурентов видно, куда они вкладываются, по тендерам видны реальные бюджеты, а по истории изменений в документации понятно, что просили клиенты. Шаг 4. Разбираю нишу Excalidraw вместе с ИИ Этот разбор занял у меня один вечер. Claude с веб-поиском искал и сводил источники, я проверяла. Раньше на такое уходило несколько недель. Сначала я попросила найти, что люди пишут про Excalidraw на своих серверах. Вот что нашлось. У проекта 135 тысяч звёзд на GitHub и лицензия MIT, так что на этом коде можно легально строить свой продукт. Если поставить Excalidraw на свой сервер, совместное редактирование и ссылки из коробки не работают. Запрос на это висит в репозитории уже много лет. Сообщество собирает свои сборки с совместной работой. Хостинги продают развёртывание Excalidraw с совместной работой в один клик, от €9 в месяц. По таблице выше это сигнал второй силы, за решение уже платят. Для воркшопов с таймерами и голосованием в сравнениях советуют Miro. В Excalidraw этих функций нет. Потом я посмотрела на конкурентов. У Яндекс Досок с февраля 2025 года есть версия для серверов крупных компаний, но эстетика у неё другая. Excalidraw+ версию для своего сервера ещё разрабатывает, и она войдёт в корпоративную лицензию. Для tldraw в продакшне нужен лицензионный ключ, а коммерческую лицензию продают через отдел продаж. Из этого получилась гипотеза. Доска в эстетике Excalidraw с функциями для воркшопов, как в Miro, которую компания ставит на свой сервер в России. Если пройтись по вопросам YC, проблема острая для узкой группы, конкуренты есть, похожие бизнесы зарабатывают, а окно открылось недавно. Факты ИИ собирает быстро, но оценивать их всё равно человеку. McKinsey проверили это на 40 продакт-менеджерах. С ИИ продуктивность выросла на 40%, опытные сохранили качество, а начинающие стали работать быстрее, но хуже. ИИ уверенно путает даты, версии и цены, поэтому каждый факт приходится сверять с первоисточником. В моём разборе часть данных в пересказах оказалась устаревшей, и на проверку ушла заметная часть вечера. Шаг 5. Найдите преимущество, которое не скопируют В стартапах такое преимущество называют moat, то есть ров. Конкурент не сможет быстро его повторить, даже если захочет. Корпорация считает рынок миллиардами и отчитывается перед акционерами каждый квартал. Сегмент в несколько тысяч команд ей невыгоден, даже если она его заметила. А маленькая команда может годами копать нишу, которая ей самой интересна. Excalidraw, кстати, начался 1 января 2020 года с твита инженера Facebook о небольшом приложении для рисования. Маленькие команды ещё и быстрее берут в работу новые инструменты. Anthropic разобрали 500 тысяч диалогов про код. По предварительной оценке, 33% диалогов в Claude Code касались работы стартапов и только 13% задач крупных компаний. Если вы знаете нишу изнутри и пользователи вам доверяют, крупному конкуренту понадобятся годы, чтобы это догнать. Шаг 6. Проверьте деньгами, без программиста Пол Бакхайт, один из партнёров YC, советует командам с сомнительной идеей сначала попробовать её продать, а строить потом. Пока продаёшь, часто находится настоящая идея. Первую проверку можно провести за выходные с лендингом, листом ожидания и предзаказом. В зачёт идут только оплаты и предзаказы. Прототип тоже соберёт ИИ. Примерно у четверти стартапов зимнего набора YC 2025 года 95% кода написал ИИ, а весь набор рос в сумме на 10% в неделю. Без инженера не обойтись там, где появляются пользователи и данные. Чтобы совместное редактирование Excalidraw заработало на своём сервере, нужны отдельный сервер для совместной работы и HTTPS, и настраивать это лучше инфраструктурному инженеру. Маркетинг без бюджета Реклама для российского рынка ограничена, а на большие бюджеты у маленькой команды нет денег. Зато можно попасть в момент или сделать так, чтобы продуктом делились сами пользователи. Когда Miro уходил, Pruffme сделал импорт досок. Сначала через него перенесли 146 досок, а к августу 2024 года уже 12 тысяч. Excalidraw разошёлся по Twitter за две недели после поста автора о первых результатах. Компания из одного человека В мае 2025 года Дарио Амодеи, CEO Anthropic, предсказал, что в 2026 году появится первая компания стоимостью миллиард долларов с одним сотрудником. Инструменты для такой компании уже есть у всех. Но в Массачусетском технологическом институте заметили, что люди, которые пишут с ChatGPT, хуже помнят свой текст и меньше считают его своим. С нишей то же самое. Если только пересылать выводы модели, рынок вы так и не узнаете и не отличите настоящий сигнал от уверенной ошибки. Нейросеть найдёт и разложит источники, а читать их, сомневаться и решать, что строить, придётся вам. Автор продукта и его первый пользователь всё равно вы.
Architecture விளக்க Excalidraw போதும். கணக்கு தேவையில்லை. உலாவியில் வரையலாம். கையால் வரைந்தது போல் தோன்றும். அடுத்த வரைபடத்தை இதில் வரைவீர்களா ? excalidraw/excalidraw Instagram-லும் பின்தொடருங்கள்: instagram.com
10 Repositorios de GitHub que Reemplazan Suscripciones Caras (100% Open Source) ¿Cuánto dinero se te va cada mes pagando suscripciones de software que apenas usas al máximo? Calendly, DocuSign, Miro, Airtable, Zapier, Firebase o Google Fotos parecen baratos por separado, pero juntos representan un impuesto silencioso de más de $2,400 dólares al año sobre tu trabajo. En este video analizamos en pizarra 10 repositorios de código abierto (Open Source) en GitHub que juntos suman casi 800,000 estrellas y que reemplazan directamente a las plataformas SaaS más caras del mercado. Veremos cómo funciona su arquitectura paso a paso, qué problema resuelven y cuál es la realidad técnica de autohospedarlos (Self-Hosting) en tu propio servidor. ⏱️ MARCAS DE TIEMPO (CAPÍTULOS): 00:00 El Impuesto Silencioso de las Suscripciones SaaS ($2,400/año) 01:05 #10 Documenso (Alternativa Open Source a DocuSign) 01:40 #9 Cal.com (Alternativa Open Source a Calendly) 02:13 #8 PostHog (Alternativa a Amplitude y Mixpanel) 02:55 #7 Penpot (Alternativa Open Source a Figma) 03:32 #6 NocoDB (Alternativa Open Source a Airtable) 04:08 #5 Coolify (Alternativa a Vercel, Netlify y Heroku) 04:38 #4 Excalidraw (Alternativa a Miro y Lucidchart) 05:16 #3 Supabase (Alternativa Open Source a Google Firebase) 05:48 #2 n8n (Alternativa a Zapier y Make para Automatización e IA) 06:23 #1 Immich (Alternativa privada a Google Fotos e iCloud) 07:03 Menciones Honoríficas (Plausible, AppFlowy, Listmonk, Ollama) y la Verdad del Self-Hosting 🔗 REPOSITORIOS MENCIONADOS EN EL VIDEO: • #10 Documenso: documenso/documenso • #9 Cal.com: calcom/cal.com • #8 PostHog: PostHog/posthog • #7 Penpot: penpot/penpot • #6 NocoDB: nocodb/nocodb • #5 Coolify: coollabsio/coolify • #4 Excalidraw: excalidraw/excalidraw • #3 Supabase: supabase/supabase • #2 n8n: n8n-io/n8n • #1 Immich: immich-app/immich ⚡ MENCIONES EXTRA: • Plausible Analytics: plausible/analytics • AppFlowy: AppFlowy-IO/AppFlowy • Listmonk: knadh/listmonk • Ollama: ollama/ollama 🔔 Suscríbete al canal para más tutoriales prácticos sobre inteligencia artificial, automatización, código abierto y laboratorios virtuales explicados paso a paso: 💣 Suscríbete aquí 👉 youtube.com #GitHub #OpenSource #SelfHosting #n8n #Supabase #Docker #Automatizacion #InteligenciaArtificial #SoftwareLibre #AlbertoIA
把大佬的宝藏做成了一个开箱即用的skill,大家可以来玩呀 GitHub:JuniverseLi/motion-studio-skill
Sklonuj DOWOLNĄ Appkę z Claude Code i REA (BEZ KODOWANIA) Zbuduj własną stronę z Claude Code i REA (inżynieria wsteczna) - bez programowania - i wgraj ją na sprawdzony hosting: hostinger.com Film zawiera fragment sponsorowany platformy Hostinger. Z kodem ROBERT10 otrzymasz 10% zniżki na wszystkie plany 12, 24 oraz 48-miesięczne. Kod łączy się z innymi promocjami. Repozytorium REA (Reverse Engineer Anything) w kilka dni podbiło GitHuba, a bańka AI oszalała na punkcie obietnicy, że Twój agent w Claude Code czy Codex rozbierze dowolną aplikację na części pierwsze i zbuduje jej własną wersję... ale czy faktycznie tak jest? Postanowiłem to sprawdzić! Dziś pokażę Ci, jak wygląda inżynieria wsteczna (reverse engineering) z Claude Code i REA w praktyce. Bez pisania kodu. Sprawdzimy REA, czyli serwer MCP oparty na silniku Ghidra, na 4 różnych aplikacjach, które rozbierzemy na czynniki pierwsze, a następnie zrekonstruujemy z pomocą Claude Code. Weźmiemy na warsztat: aplikację Clop na macOS i na jej przykładzie pokażę Ci cały proces, który przechodzi Claude Code, aby stworzyć nową wersję appki z REA. Następnie weźmiemy na tapet Excalidraw i zbudujemy nasz odpowiednik "Bazgroł", który następnie opublikujemy do sieci z poziomu rozmowy w Claude Code. Zobaczymy też, czy Claude Code poradzi sobie ze stworzeniem modów do gry Slay the Spire 2, a na koniec zbudujemy widget do OBS z limitami Claude Code, inspirowany CodexBar. Zobaczysz, jak w praktyce wygląda praca z REA w Claude Code - od pierwszego prompta, przez mapę aplikacji i śledztwo z dowodami, aż po raport, na podstawie którego Claude pisze własny kod i tworzy design dla nowej aplikacji. Po drodze sprawdzimy uczciwie, ile z tej pracy faktycznie robi REA, a ile zrobiłby sam Claude Code bez tego narzędzia. Na koniec odpowiem szczerze na pytanie, czy do zbudowania własnej wersji danej appki REA jest Ci w ogóle potrzebne. ======================================= 🔗 Linki z filmu: 👉🏻 Operatorzy AI (zniżka -77%) - w środku gotowy skill do inżynierii wstecznej bez REA: link.creatorhub.pl 👉🏻 REA (Reverse Engineer Anything) - prawdziwe repo serwera MCP (uważaj na podróbki): morluto/rea 👉🏻 Clop - appka do kompresji obrazków i wideo na macOS: lowtechguys.com 👉🏻 Repo Excalidraw (MIT) - sprawdzian, czy REA nie zmyśla: excalidraw/excalidraw 👉🏻 Bazgroł - moja tablica do rysowania (porysuj sam): bazgrol.zeroklikania.pl 👉🏻 Thumbforge - moja appka do miniatur (lista oczekujących): link.creatorhub.pl 👉🏻 Oficjalny plugin i konektor Hostinger do Claude Code: hostinger.com 👉🏻 Szablon moda do Slay the Spire 2: Alchyr/ModTemplate-StS2 👉🏻 CodexBar - limity Claude Code i Codex w pasku menu: steipete/CodexBar 👉🏻 CodeNotch - limity agentów AI przy krawędzi ekranu: vinzdg/codenotch ======================================= 📺 Zobacz też: Hostinger z poziomu Claude Code: youtu.be 6 lekcji vibe codingu appki: youtu.be Claude Design - strony i appki z AI: youtu.be Własna appka z agentem Codex: youtu.be ======================================= 🔗 Dodatkowa wiedza za free: 🌐 Moje linki i materiały: creatorhub.pl 📰 Bezpłatny newsletter o technologiach: link.creatorhub.pl ======================================= 🛠️ Inne polecane narzędzia: ➡️ Serwer VPS Hostinger: postaw Claude Code, Codex czy n8n na własnym serwerze: hostinger.com Z kodem ROBERTHOST otrzymasz 10% zniżki ➡️ Wispr Flow: mowa na tekst, 30 dni za free: link.creatorhub.pl ➡️ Setapp: 250 aplikacji na macOS za darmo: link.creatorhub.pl ➡️ Screen Studio: nagrywanie pulpitu na macOS: link.creatorhub.pl ➡️ ego lite: przeglądarka agentowa na macOS: link.ego.app ======================================= ⌚ Spis Treści: 00:00 Wprowadzenie 00:57 Czym jest REA (Reverse Engineer Anything) 02:25 Reverse engineering krok po kroku 08:26 Własna wersja Excalidraw z REA 10:33 Fragment sponsorowany 13:11 Wgrywanie appki na hosting 15:01 Mod do Slay the Spire 2 17:52 Widget limitów Claude Code w OBS 20:35 Czy warto korzystać z REA ======================================= ➡️ Masz przeczucie, że razem możemy zrobić coś fajnego, a może potrzebujesz warsztatu lub pomocy przy wdrożeniu AI w swoim biznesie? Śmiało odezwij się do mnie na maila: → robert@omaai.pl Miniatura tego filmu została wygenerowana przy użyciu AI. #rea #claudecode #vibecoding [en-US] Clone ANY App with Claude Code and REA (NO CODING) Build your own website with Claude Code and REA (reverse engineering)—no coding required—and upload it to trusted hosting: hostinger.com This video contains a sponsored piece from Hostinger. Use the code ROBERT10 to get a 10% discount on all 12-, 24-, and 48-month plans. This code can be combined with other promotions. The REA (Reverse Engineer Anything) repository took GitHub by storm in just a few days, and the AI bubble went wild with the promise that your agent at Claude Code or Codex would disassemble any application and build its own version... but is it really possible? I decided to put it to the test! Today I'll show you what reverse engineering with Claude Code and REA looks like in practice. Without writing any code. We'll test REA, an MCP server based on the Ghidra engine, on four different apps, which we'll disassemble and then reconstruct using Claude Code. We'll take a look at the Clop app for macOS, and using it as an example, I'll show you the entire process Claude Code goes through to create a new version of an app with REA. Next, we'll take Excalidraw and build our "Scribble" equivalent, which we'll then publish online from a conversation in Claude Code. We'll also see if Claude Code can create mods for Slay the Spire 2, and finally, we'll build a widget for OBS with Claude Code limits, inspired by CodexBar. You'll see what working with REA looks like in practice in Claude Code—from the first prompt, through the application map and evidence investigation, to the report, based on which Claude writes his own code and creates the design for the new app. Along the way, we'll honestly examine how much of this work REA actually does, and how much Claude Code would have done without it. Finally, I'll answer honestly whether you even need REA to build your own version of a given app. =========================================== 🔗 Links from the video: 👉🏻 AI Operators (-77% discount) - inside a ready-made skill for reverse engineering without REA: link.creatorhub.pl 👉🏻 REA (Reverse Engineer Anything) - real MCP server repo (beware of fakes): morluto/rea 👉🏻 Clop - an app for compressing images and videos on macOS: lowtechguys.com 👉🏻 Excalidraw (MIT) repo - a test to see if REA is making things up: excalidraw/excalidraw 👉🏻 Scribble - my drawing board (draw it yourself): bazgrol.zeroklikania.pl 👉🏻 Thumbforge - my thumbnail app (waiting list): link.creatorhub.pl 👉🏻 Official Hostinger plugin and connector for Claude Code: hostinger.com 👉🏻 Mod template for Slay the Spire 2: Alchyr/ModTemplate-StS2 👉🏻 CodexBar - Claude Code and Codex limits in the menu bar: steipete/CodexBar 👉🏻 CodeNotch - AI agent limits at the edge of the screen: vinzdg/codenotch =========================================== 📺 See also: Hostinger from Claude Code: youtu.be 6 lessons on vibe coding apps: youtu.be Claude Design - websites and apps with AI: youtu.be Your own app with the Codex agent: youtu.be ===================================================== 🔗 Additional knowledge for free: 🌐 My Links and resources: creatorhub.pl 📰 Free technology newsletter: link.creatorhub.pl =========================================== 🛠️ Other recommended tools: ➡️ Hostinger VPS server: host Claude Code, Codex, or n8n on your own server: hostinger.com Use code ROBERTHOST for a 10% discount ➡️ Wispr Flow: speech to text, 30 days free: link.creatorhub.pl ➡️ Setapp: 250 macOS apps for free: link.creatorhub.pl ➡️ Screen Studio: desktop recording on macOS: link.creatorhub.pl ➡️ ego lite: agent-based browser for macOS: link.ego.app ============================================ ⌚ Table of Contents: 00:00 Introduction 00:57 What is REA (Reverse Engineer Anything)? 02:25 Reverse engineering step by step 08:26 Your own version of Excalidraw with REA 10:33 Sponsored clip 13:11 Uploading the app to a hosting site 15:01 Slay the Spire 2 mod 17:52 Claude Code limits widget in OBS 20:35 Is it worth using? REA =========================================== ➡️ Do you have a feeling that we could do something cool together, or maybe you need a workshop or help implementing AI in your business? Feel free to contact me by email: → robert@omaai.pl The thumbnail for this video was generated using AI. #rea #claudecode #vibecoding