diff --git a/CHANGELOG.md b/CHANGELOG.md index a464dbdb..15012a23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,11 +18,12 @@ because it turns other people's test suites red. - **How much memory a run may use is yours to set.** `--memory` on `generate`, `validate` and `preset show`, and Memory on the window's - Preferences screen: `auto` (half of what this machine or container gives - the program), a size such as `8gb`, or `off`. Pictures and films wait for - room rather than all being held at once. A plan is refused at this - setting rather than at a fixed 2 GiB, and `auto` never goes below that. - Files come out byte for byte the same. + Preferences screen: `auto` (half of this machine or container), a size + such as `8gb`, or `off`. Near that amount the program frees memory more + often and runs slower, but a run that needs more still gets it. A plan + above it is refused, and under `auto` never below 2 GiB. In a container of + 1 GB, 500 000 text files are written where 400 000 were killed. Files come + out byte for byte the same. - **A Docker image.** `ghcr.io/donislawdev/tfg` holds the Linux command line of a release and nothing else, for x64 and Arm, and runs with no network. It diff --git a/README.md b/README.md index 4bed31dd..ed457076 100644 --- a/README.md +++ b/README.md @@ -345,7 +345,7 @@ tfg generate --format txt --size 1mb settings come from the flags | `--clean` | turn off the self describing label written inside each file | | `--dry-run` | count and show, write nothing at all | | `--json` | write the manifest to standard output | -| `--memory ` | how much memory the run may use: `auto` (the default, half of what this machine gives the program), a size such as `8gb`, or `off` for no limit. Pictures wait for room rather than all being held at once, and a plan larger than this is refused before anything is written. Under `auto`, a plan of up to 2.0 GB is never refused. Also on `validate` and `preset show` | +| `--memory ` | how much memory the run may use: `auto` (the default, half of what this machine gives the program), a size such as `8gb`, or `off` for no limit. Pictures wait for room rather than all being held at once, and a plan larger than this is refused before anything is written. When a run gets close to this amount, the program frees memory more often, which makes the run slower. A run that needs more memory than this still gets it. Under `auto`, a plan of up to 2.0 GB is never refused. Also on `validate` and `preset show` | ### `tfg validate` diff --git a/cmd/tfg-gui/main.go b/cmd/tfg-gui/main.go index 5fcf1f82..ea6dfa7f 100644 --- a/cmd/tfg-gui/main.go +++ b/cmd/tfg-gui/main.go @@ -8,9 +8,14 @@ package main import ( "os" + "github.com/donislawdev/TestingFilesGenerator/internal/engine" "github.com/donislawdev/TestingFilesGenerator/internal/gui" ) func main() { + // This process is the window's own, so the memory setting holds each + // whole run and not only its plan - engine/processmemory.go says why only + // the two main functions say so. + engine.OwnProcess() os.Exit(gui.Run(os.Args[1:], os.Stderr)) } diff --git a/cmd/tfg/main.go b/cmd/tfg/main.go index 983560fb..a83da1d2 100644 --- a/cmd/tfg/main.go +++ b/cmd/tfg/main.go @@ -14,6 +14,7 @@ import ( "syscall" "github.com/donislawdev/TestingFilesGenerator/internal/cli" + "github.com/donislawdev/TestingFilesGenerator/internal/engine" ) func main() { @@ -29,6 +30,11 @@ func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() + // This process is the command's own, so --memory holds the whole run and + // not only its plan. Here and in the window's main, and nowhere a test + // reaches - engine/processmemory.go says why. + engine.OwnProcess() + signals := make(chan os.Signal, 1) signal.Notify(signals, os.Interrupt, syscall.SIGTERM) defer signal.Stop(signals) diff --git a/internal/cli/memoryflag.go b/internal/cli/memoryflag.go index 5e8f67f3..0b970d67 100644 --- a/internal/cli/memoryflag.go +++ b/internal/cli/memoryflag.go @@ -23,6 +23,8 @@ import ( var memoryHelp = "how much memory this run may use: auto (half of what this machine gives the program), " + "a size such as 8gb, or off for no limit. Pictures wait for room rather than all being held at once, " + "and a plan larger than this is refused before anything is written. " + + "When a run gets close to this amount, the program frees memory more often, which makes the run slower. " + + "A run that needs more memory than this still gets it. " + "Under auto, a plan of up to " + core.HumanBytes(core.MaxPlanBytes) + " is never refused" // memoryValue is the flag's value, auto until somebody writes one. diff --git a/internal/core/memory.go b/internal/core/memory.go index 12399677..c09c94e3 100644 --- a/internal/core/memory.go +++ b/internal/core/memory.go @@ -199,3 +199,27 @@ func (m Memory) On(machine func() (int64, error)) MemoryLimits { half := total / 2 return MemoryLimits{Run: half, Plan: max(half, MaxPlanBytes)} } + +// ForRuntime is the memory limit a process that is the program's own hands the +// Go runtime for a run under these limits. start is the limit the process +// started with - what GOMEMLIMIT said, or no limit at all. +// +// The runtime's limit is soft. Close to it the collector runs more often, a +// run that needs more still gets it, and the collector takes no more than +// about half the processor, so a run held under it is at most about twice as +// slow (go.dev/doc/gc-guide, the memory limit section). Well under it nothing +// changes. Without it a run takes about two and a half times what it holds, +// and in a container of 1 GB that was the difference between 400 000 files +// killed by the system and 500 000 written (docs/FILE-CEILING-2026-11-02.md +// section 12). +// +// Never above start, because a person who set GOMEMLIMIT lower chose that on +// purpose. Off hands start back, and so does a machine that could not be +// asked: its Run is MaxPlanBytes, a number about no machine, and on a large +// machine whose memory could not be read it would slow a run for nothing. +func (l MemoryLimits) ForRuntime(start int64) int64 { + if l.Run <= 0 || l.Unknown != nil { + return start + } + return min(l.Run, start) +} diff --git a/internal/engine/engine.go b/internal/engine/engine.go index 13344200..9da9f2d0 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -401,7 +401,9 @@ func PlanContext(ctx context.Context, targets []Target, opt Options) ([]PlannedF // Watches what the plan costs while it is being built. Constructed here // rather than at the first file because constructing it is what takes the // reference point, and that has to happen before any of the plan exists. - pl.budget = newPlanMemory(opt.Memory, opt.memoryLimits()) + limits := opt.memoryLimits() + pl.budget = newPlanMemory(opt.Memory, limits) + handToRuntime(limits) // The manifest lands beside the files, so its name is a name too. A path // here would leave a manifest outside the directory the run was pointed diff --git a/internal/engine/processmemory.go b/internal/engine/processmemory.go new file mode 100644 index 00000000..a6a85c72 --- /dev/null +++ b/internal/engine/processmemory.go @@ -0,0 +1,67 @@ +package engine + +import ( + "runtime/debug" + + "github.com/donislawdev/TestingFilesGenerator/internal/core" +) + +// The memory setting handed to the Go runtime, in a process that is the +// program's own. +// +// The setting said how much memory a run may take and held only the plan to +// it. The rest of a run - the plan, the manifest entries and what the writers +// throw away - was left to the collector's default, which lets the heap grow +// to about twice what is live before it collects, and a run took about two and +// a half times what it held. In a container of 1 GB that killed a run of +// 400 000 text files under auto, and one of three runs of 300 000 +// (docs/FILE-CEILING-2026-11-02.md section 12). +// +// Plain variables rather than a lock, because both are written once, by the +// main function, before it starts anything that plans - and a goroutine +// started after a write sees it. A lock here would be concurrency the guard of +// the concurrent files has to be told about, defending against a caller that +// does not exist. +var ( + owned bool + // startLimit is the runtime's limit when the process said it was the + // program's own. + startLimit int64 +) + +// OwnProcess says this process is the program's own - the command line or the +// window, with nothing beside it that a limit on its memory would hold back - +// so every plan from now on hands its memory setting to the Go runtime as the +// limit of the whole process. Only the two main functions call it. +// +// Not done for every caller of the engine, because the limit is the process's +// and not the run's. The guards plan hundreds of runs side by side in one test +// binary, some under a setting of a few megabytes, and handing that to the +// runtime would hold every guard beside them to it. +// +// Never undone. The command line ends with its one run, and the window hands +// over the setting of each run it plans, so a changed setting is in force from +// the next run and off gives the runtime back the limit it started with. +// +// Called first, before anything that plans starts. A second call changes +// nothing, so the limit it started with is never taken from a run. +func OwnProcess() { + if owned { + return + } + startLimit = debug.SetMemoryLimit(-1) + owned = true +} + +// handToRuntime gives the runtime the limit for a run under limits, in a +// process that is the program's own, and does nothing anywhere else. +// +// Called when a plan starts rather than when the writing does, because the +// plan is the first thing both surfaces do for a run and the manifest is saved +// after the writing has returned - a limit set and taken back inside Run would +// leave both outside it. +func handToRuntime(limits core.MemoryLimits) { + if owned { + debug.SetMemoryLimit(limits.ForRuntime(startLimit)) + } +} diff --git a/internal/guard/mutation/entries.py b/internal/guard/mutation/entries.py index 65c6f68e..8db2edeb 100644 --- a/internal/guard/mutation/entries.py +++ b/internal/guard/mutation/entries.py @@ -16038,7 +16038,7 @@ # the ceiling that could not be moved, which is R02. "planning ignores the memory setting and works to the old ceiling", "internal/engine/engine.go", - "pl.budget = newPlanMemory(opt.Memory, opt.memoryLimits())", + "pl.budget = newPlanMemory(opt.Memory, limits)", "pl.budget = newPlanMemory(opt.Memory, core.MemoryLimits{Plan: core.MaxPlanBytes})", "TestThePlanCeilingIsTheMemorySetting", ), @@ -16319,4 +16319,46 @@ "\tpl.reserve = planCapacity(targets, pl.budget)\n\tpl.out = make([]PlannedFile, 0, pl.reserve)\n", "TestARunRefusedAtItsFirstFileReservesNothing", ), + ( + "the runtime is handed the setting even above the limit the process started with", + "internal/core/memory.go", + "\treturn min(l.Run, start)\n", + "\treturn l.Run\n", + "TestTheMemorySettingIsTheLimitTheRuntimeIsHanded", + ), + ( + "a machine that could not be asked hands the runtime a number about no machine", + "internal/core/memory.go", + "\tif l.Run <= 0 || l.Unknown != nil {\n\t\treturn start\n", + "\tif l.Run <= 0 {\n\t\treturn start\n", + "TestTheMemorySettingIsTheLimitTheRuntimeIsHanded", + ), + ( + "every plan hands its setting to the runtime, also in a process that is not the program's own", + "internal/engine/processmemory.go", + "\tif owned {\n\t\tdebug.SetMemoryLimit(", + "\tif true {\n\t\tdebug.SetMemoryLimit(", + "TestAPlanOutsideTheProgramsOwnProcessLeavesTheRuntimeAlone", + ), + ( + "a plan no longer hands its memory setting to the runtime", + "internal/engine/engine.go", + "\thandToRuntime(limits)\n", + "\t_ = limits\n", + "TestTheCommandLineHoldsTheWholeRunToItsMemorySetting", + ), + ( + "the command line does not say its process is the program's own", + "cmd/tfg/main.go", + "\tengine.OwnProcess()\n", + "\t_ = engine.OwnProcess\n", + "TestTheCommandLineHoldsTheWholeRunToItsMemorySetting", + ), + ( + "the window does not say its process is the program's own", + "cmd/tfg-gui/main.go", + "\tengine.OwnProcess()\n", + "\t_ = engine.OwnProcess\n", + "TestTheWindowSaysItsProcessIsItsOwnBeforeItOpens", + ), ] diff --git a/internal/guard/runtimememory_test.go b/internal/guard/runtimememory_test.go new file mode 100644 index 00000000..240599af --- /dev/null +++ b/internal/guard/runtimememory_test.go @@ -0,0 +1,193 @@ +package guard + +import ( + "bytes" + "errors" + "go/ast" + "go/parser" + "go/token" + "math" + "os" + "os/exec" + "path/filepath" + "runtime/debug" + "strings" + "testing" + + "github.com/donislawdev/TestingFilesGenerator/internal/core" + "github.com/donislawdev/TestingFilesGenerator/internal/engine" +) + +// The memory setting is the limit the Go runtime is handed, and the number is +// the setting's - never above what the process started with. +// +// Before 2026-11-02 the setting held only the plan, and a run took about two +// and a half times what it held: in a container of 1 GB, 400 000 text files +// were killed by the system, and with the setting handed over 500 000 were +// written (docs/FILE-CEILING-2026-11-02.md section 12). Asked of the number +// rather than of a process, so no limit is set in this test binary. +func TestTheMemorySettingIsTheLimitTheRuntimeIsHanded(t *testing.T) { + const gib = int64(1) << 30 + const none = int64(math.MaxInt64) + machine := func(n int64) func() (int64, error) { return func() (int64, error) { return n, nil } } + unreadable := func() (int64, error) { return 0, errors.New("not asked") } + setting := func(text string) core.Memory { + m, err := core.ParseMemory(text) + if err != nil { + t.Fatalf("%q does not read as a memory setting: %v", text, err) + } + return m + } + cases := []struct { + name string + setting core.Memory + machine func() (int64, error) + start int64 + want int64 + }{ + {"auto in a container of 1 GB", setting("auto"), machine(gib), none, gib / 2}, + {"a size of its own", setting("8gb"), machine(32 * gib), none, 8 * gib}, + {"auto under a lower GOMEMLIMIT", setting("auto"), machine(32 * gib), gib, gib}, + {"off", setting("off"), machine(gib), none, none}, + {"off under a GOMEMLIMIT", setting("off"), machine(gib), gib, gib}, + {"auto on a machine that could not be asked", setting("auto"), unreadable, none, none}, + } + for _, c := range cases { + if got := c.setting.On(c.machine).ForRuntime(c.start); got != c.want { + t.Errorf("%s: the runtime is handed %d B, not %d B", c.name, got, c.want) + } + } +} + +// And a plan in a process that is not the program's own leaves the runtime's +// limit as it was. +// +// This test binary is such a process. Its guards plan side by side, some under +// a setting of a few megabytes, and one of them handing that to the runtime +// would hold every guard beside it to it - slowly, and green. +func TestAPlanOutsideTheProgramsOwnProcessLeavesTheRuntimeAlone(t *testing.T) { + memory, err := core.ParseMemory("64mb") + if err != nil { + t.Fatal(err) + } + before := debug.SetMemoryLimit(-1) + if memory.On(core.MachineMemory).ForRuntime(before) == before { + t.Fatalf("a setting of 64 MB would hand the runtime the limit it already has, %d B, so this is not the case it names", before) + } + _, err = engine.Plan([]engine.Target{{ID: "files", Format: "txt", Sizes: engine.Uniform(10, 100)}}, + engine.Options{OutDir: t.TempDir(), ManifestName: "manifest.json", Memory: memory}) + if err != nil { + t.Fatalf("planning ten text files under 64 MB: %v", err) + } + if after := debug.SetMemoryLimit(before); after != before { + t.Errorf("a plan in a process that is not the program's own changed the runtime's limit from %d B to %d B", before, after) + } +} + +// The command line holds the whole run to --memory, not only its plan. +// +// Asked of the real binary, because the one line that makes the process the +// program's own is in its main and nothing in this test binary runs it. With +// the collector off, the runtime collects only when a memory limit asks it +// to, so a run that collected at all was handed one. A collection a plan asks +// for itself, to measure what it holds, is marked forced in the trace and is +// not counted (pkg.go.dev/runtime, GODEBUG gctrace). +// +// What this does not hold: the window's main. The window binary needs a +// display to run a plan, so the guard below reads its source instead. +func TestTheCommandLineHoldsTheWholeRunToItsMemorySetting(t *testing.T) { + binary := buildCommandLine(t) + collections := func(memory string) int { + t.Helper() + cmd := exec.Command(binary, "generate", "--format", "txt", "--size", "4kb", "--count", "20000", + "--memory", memory, "--dry-run", "--out", filepath.Join(t.TempDir(), "out")) + cmd.Env = append(withoutRuntimeSettings(os.Environ()), "GOGC=off", "GODEBUG=gctrace=1") + var stderr bytes.Buffer + cmd.Stderr = &stderr + if err := cmd.Run(); err != nil { + t.Fatalf("tfg generate --memory %s: %v\n%s", memory, err, stderr.String()) + } + n := 0 + for _, line := range strings.Split(stderr.String(), "\n") { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "gc ") && strings.Contains(line, " MB goal") && !strings.HasSuffix(line, "(forced)") { + n++ + } + } + return n + } + if n := collections("off"); n != 0 { + t.Fatalf("with the collector off and --memory off, %d collections ran anyway, so a collection here does not say a limit was handed", n) + } + if n := collections("48mb"); n == 0 { + t.Errorf("a run of 20 000 files under --memory 48mb with the collector off never collected. " + + "The setting did not reach the runtime, so it held the plan and nothing else") + } +} + +// The window holds each whole run to its memory setting too - asked of its +// main, because the window binary needs a display to plan anything and the +// guard above cannot run it. +// +// What is held here is the order: the call that makes the process the +// program's own is a statement of main, and it comes before the window opens. +// gui.Run returns only when the window closes, so a call after it would leave +// every run of the window to the runtime's default, and nothing else would turn +// red - the window's runs that guards plan are planned in this test binary, +// which is never the program's own. What the call does is the command line +// guard's to hold, because it is the same call. +func TestTheWindowSaysItsProcessIsItsOwnBeforeItOpens(t *testing.T) { + path := filepath.Join(repoRoot(t), "cmd", "tfg-gui", "main.go") + parsed, err := parser.ParseFile(token.NewFileSet(), path, nil, 0) + if err != nil { + t.Fatalf("reading the window's main: %v", err) + } + var body []ast.Stmt + for _, d := range parsed.Decls { + if fn, ok := d.(*ast.FuncDecl); ok && fn.Recv == nil && fn.Name.Name == "main" { + body = fn.Body.List + } + } + owns, opens := -1, -1 + for i, stmt := range body { + if s, ok := stmt.(*ast.ExprStmt); ok && owns < 0 { + if call, ok := s.X.(*ast.CallExpr); ok && calledAs(call, "engine", "OwnProcess") { + owns = i + } + } + ast.Inspect(stmt, func(n ast.Node) bool { + if call, ok := n.(*ast.CallExpr); ok && opens < 0 && calledAs(call, "gui", "Run") { + opens = i + } + return opens < 0 + }) + } + if opens < 0 { + t.Fatalf("the main in %s no longer calls gui.Run, so this guard cannot say what happens before the window opens. "+ + "Point it at the call that opens the window now", path) + } + if owns < 0 { + t.Fatalf("the main in %s does not call engine.OwnProcess as a statement of its own, "+ + "so the memory setting holds the plan of each run in the window and not the run", path) + } + if owns > opens { + t.Errorf("the main in %s calls engine.OwnProcess as statement %d, after the window opens at statement %d. "+ + "gui.Run returns when the window closes, so no run of the window is held to its memory setting", path, owns+1, opens+1) + } +} + +// withoutRuntimeSettings is the environment without the variables that set the +// Go runtime's collector, so a machine that sets them does not answer for the +// binary under test. +func withoutRuntimeSettings(env []string) []string { + var kept []string + for _, kv := range env { + name, _, _ := strings.Cut(kv, "=") + switch strings.ToUpper(name) { + case "GOGC", "GOMEMLIMIT", "GODEBUG": + continue + } + kept = append(kept, kv) + } + return kept +} diff --git a/internal/guard/testdata/screens/preferences-chosen.png b/internal/guard/testdata/screens/preferences-chosen.png index 16dc4cc1..d375de12 100644 Binary files a/internal/guard/testdata/screens/preferences-chosen.png and b/internal/guard/testdata/screens/preferences-chosen.png differ diff --git a/internal/guard/testdata/screens/preferences-chosen.xml b/internal/guard/testdata/screens/preferences-chosen.xml index 4e72e83d..faa41ec1 100644 --- a/internal/guard/testdata/screens/preferences-chosen.xml +++ b/internal/guard/testdata/screens/preferences-chosen.xml @@ -113,12 +113,12 @@ - - - - + + + + Memory - + @@ -139,16 +139,18 @@ - - - + + + How much memory one run may take, the plan and the files being written together. Pictures wait for room rather than - all being held at once, and a plan larger than this is refused before anything is written. Under Automatic, a plan of up to - 2.0 GB is never refused. It applies from the next run, and nothing in the files changes. + all being held at once, and a plan larger than this is refused before anything is written. When a run gets close to this + amount, the program frees memory more often, which makes the run slower. A run that needs more memory than this + still gets it. Under Automatic, a plan of up to 2.0 GB is never refused. It applies from the next run, and nothing in the + files changes. - + Runs may take 8.0 GB, half of the memory this computer gives the program. @@ -159,7 +161,7 @@ - + diff --git a/internal/guard/testdata/screens/preferences.png b/internal/guard/testdata/screens/preferences.png index 42f893dc..6c4ec2be 100644 Binary files a/internal/guard/testdata/screens/preferences.png and b/internal/guard/testdata/screens/preferences.png differ diff --git a/internal/guard/testdata/screens/preferences.xml b/internal/guard/testdata/screens/preferences.xml index b44c3a0b..1f799392 100644 --- a/internal/guard/testdata/screens/preferences.xml +++ b/internal/guard/testdata/screens/preferences.xml @@ -113,12 +113,12 @@ - - - - + + + + Memory - + @@ -139,16 +139,18 @@ - - - + + + How much memory one run may take, the plan and the files being written together. Pictures wait for room rather than - all being held at once, and a plan larger than this is refused before anything is written. Under Automatic, a plan of up to - 2.0 GB is never refused. It applies from the next run, and nothing in the files changes. + all being held at once, and a plan larger than this is refused before anything is written. When a run gets close to this + amount, the program frees memory more often, which makes the run slower. A run that needs more memory than this + still gets it. Under Automatic, a plan of up to 2.0 GB is never refused. It applies from the next run, and nothing in the + files changes. - + Runs may take 8.0 GB, half of the memory this computer gives the program. @@ -159,7 +161,7 @@ - + diff --git a/internal/gui/text/locale/en.json b/internal/gui/text/locale/en.json index 6ba26057..18692570 100644 --- a/internal/gui/text/locale/en.json +++ b/internal/gui/text/locale/en.json @@ -753,8 +753,8 @@ }, "PreferencesMemoryScope": { "description": "Shown on the Preferences screen. Carries one value, {{.Floor}}, which has to stay spelled exactly that way.", - "hash": "sha256-847b853f524e", - "other": "How much memory one run may take, the plan and the files being written together. Pictures wait for room rather than all being held at once, and a plan larger than this is refused before anything is written. Under Automatic, a plan of up to {{.Floor}} is never refused. It applies from the next run, and nothing in the files changes." + "hash": "sha256-4c95226d14ae", + "other": "How much memory one run may take, the plan and the files being written together. Pictures wait for room rather than all being held at once, and a plan larger than this is refused before anything is written. When a run gets close to this amount, the program frees memory more often, which makes the run slower. A run that needs more memory than this still gets it. Under Automatic, a plan of up to {{.Floor}} is never refused. It applies from the next run, and nothing in the files changes." }, "PreferencesMissing": { "description": "Shown on the Preferences screen. Carries one value, {{.Tag}}, which has to stay spelled exactly that way.", diff --git a/internal/gui/text/locale/pl.json b/internal/gui/text/locale/pl.json index d4fe2f7d..2986d42b 100644 --- a/internal/gui/text/locale/pl.json +++ b/internal/gui/text/locale/pl.json @@ -149,7 +149,7 @@ "PreferencesKept": { "hash": "sha256-d5cfc5e4bd88", "other": "Między przebiegami to okno pamięta cztery rzeczy: katalog wyjściowy, rozmiar okna oraz wybrane wyżej język i pamięć. Okno wyboru folderu pamięta też ostatni pokazany folder. Nic nie opuszcza tego komputera." }, "PreferencesKeptIn": { "hash": "sha256-65ceb0c15ff4", "other": "Są przechowywane w tym folderze:" }, "PreferencesLanguageScope": { "hash": "sha256-bc4174717ca7", "other": "Język zmienia tylko okno, a nie pliki, które tworzy, przepisy, manifesty ani linię komend." }, - "PreferencesMemoryScope": { "hash": "sha256-847b853f524e", "other": "Ile pamięci może wziąć jeden przebieg, plan i zapisywane pliki razem. Obrazy czekają na miejsce, zamiast trzymać się w pamięci wszystkie naraz, a plan większy niż to ustawienie jest odrzucany, zanim cokolwiek zostanie zapisane. Przy ustawieniu Automatycznie plan do {{.Floor}} nigdy nie jest odrzucany. Działa od następnego przebiegu i nie zmienia niczego w plikach." }, + "PreferencesMemoryScope": { "hash": "sha256-4c95226d14ae", "other": "Ile pamięci może wziąć jeden przebieg, plan i zapisywane pliki razem. Obrazy czekają na miejsce, zamiast trzymać się w pamięci wszystkie naraz, a plan większy niż to ustawienie jest odrzucany, zanim cokolwiek zostanie zapisane. Gdy przebieg zbliża się do tej ilości, program częściej zwalnia pamięć, więc przebieg trwa dłużej. Przebieg, który potrzebuje więcej pamięci, i tak ją dostaje. Przy ustawieniu Automatycznie plan do {{.Floor}} nigdy nie jest odrzucany. Działa od następnego przebiegu i nie zmienia niczego w plikach." }, "PreferencesMissing": { "hash": "sha256-7679a7684a73", "other": "Język wybrany wcześniej, {{.Tag}}, nie jest dostępny w tej wersji. Okno używa języka systemu, dopóki nie wybierzesz ponownie." }, "PreferencesNextStart": { "hash": "sha256-6fee24596599", "other": "Okno mówi wybranym językiem od następnego otwarcia." }, "PreferencesRestartBusy": { "hash": "sha256-1d940383b3e9", "other": "Trwa tworzenie plików. Uruchom ponownie, gdy się skończy." }, diff --git a/internal/gui/text/preferences.go b/internal/gui/text/preferences.go index e0401efa..86e7bff2 100644 --- a/internal/gui/text/preferences.go +++ b/internal/gui/text/preferences.go @@ -35,7 +35,7 @@ func MemoryWayOff() string { return say("MemoryWayOff", "No limit") } // floor of the plan under Automatic is said from the constant that sets it, // the way --help says it. func PreferencesMemoryScope() string { - return sayf("PreferencesMemoryScope", "How much memory one run may take, the plan and the files being written together. Pictures wait for room rather than all being held at once, and a plan larger than this is refused before anything is written. Under Automatic, a plan of up to {{.Floor}} is never refused. It applies from the next run, and nothing in the files changes.", + return sayf("PreferencesMemoryScope", "How much memory one run may take, the plan and the files being written together. Pictures wait for room rather than all being held at once, and a plan larger than this is refused before anything is written. When a run gets close to this amount, the program frees memory more often, which makes the run slower. A run that needs more memory than this still gets it. Under Automatic, a plan of up to {{.Floor}} is never refused. It applies from the next run, and nothing in the files changes.", map[string]any{"Floor": HumanBytes(core.MaxPlanBytes)}) }