From 7e11d394cf924cf367775f983ceaa09f372e8c3e Mon Sep 17 00:00:00 2001 From: Max Horn Date: Mon, 17 Aug 2026 09:45:40 +0200 Subject: [PATCH 1/3] Let one polymake process serve the whole session Starting polymake costs about 0.85s per call, and measuring where it goes shows only ~40ms of that is the autoconfiguration that --config-path "" forces to re-run; the rest is loading the rules of an application, which happens whatever the configuration. So the way to stop paying it is to stop restarting polymake. polymake reads instructions from standard input when invoked as `polymake -`, so polymaking now keeps one such process per session, driven through InputOutputLocalProcess, and asks it to evaluate each call. Results continue to go to a file, so the pipe only ever carries a marker line; there is no framing protocol to get wrong and no risk of a large result filling the pipe buffer. pm.pl gained two entry points, polymaking_setup and polymaking_eval, and calls them itself when given arguments, so one-shot mode is unchanged and remains the fallback whenever the process cannot be started or has died. Preferences are applied once per application rather than once per call, since in a long lived process polymake objects to being told the same preference twice. Measured, with a session that makes 40 calls: 3.5s rather than 36s. The marginal cost of a call falls from ~870ms to ~29ms. hapcryst's test suite, unmodified, goes from 19.1s to 5.1s. The stream does not survive into another session, so it is dropped after restoring a workspace and closed at exit. Talking to a closed stream raises an error rather than returning fail, so the exchange is guarded and a polymake that has gone away is replaced rather than reported. Set the new PolymakePersistent preference to false to start polymake afresh for every call. Co-Authored-By: Claude Opus 5 --- CHANGES.md | 5 ++ doc/environment.xml | 14 +++- lib/environment.gi | 177 ++++++++++++++++++++++++++++++++++++++++---- lib/pm.pl | 117 +++++++++++++++++------------ lib/userpref.gi | 20 ++++- tst/persistent.tst | 51 +++++++++++++ 6 files changed, 319 insertions(+), 65 deletions(-) create mode 100644 tst/persistent.tst diff --git a/CHANGES.md b/CHANGES.md index 1cc3d40..23da442 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,10 @@ 0.9.0 (unreleased) +- one polymake process now serves the whole GAP session instead of a fresh one + being started for every call, which makes a session of 40 calls about ten + times faster. Set the new `PolymakePersistent` preference to `false` for the + old behaviour. + - polymake 4.0 or newer is now required, and the GAP package json is a new dependency; json needs GAP 4.12, so that is now polymaking's minimum too. polymaking now writes and reads polymake's own JSON data format instead of the pre-4 plain format, which means polymake no longer converts diff --git a/doc/environment.xml b/doc/environment.xml index 5e3ee1e..bc5461a 100644 --- a/doc/environment.xml +++ b/doc/environment.xml @@ -23,9 +23,12 @@ standard output keeps the results clear of anything polymake prints. Using custom scripts is not supported.
-Every call to polymake will re-start the program anew. This causes considerable overhead. -The number of calls to polymake is reduced by caching the results in the so-called -PolymakeObject in GAP. +Starting polymake takes the best part of a second, nearly all of it spent +loading the rules of an application. By default one polymake process therefore +serves the whole &GAP; session, reading its instructions from a pipe and +answering each in a few milliseconds; see the PolymakePersistent +preference in . The number of calls is reduced +further by caching the results in the so-called PolymakeObject in GAP. As of polymaking version 0.9.0, polymake 4.0 or newer is required. Use polymaking 0.8.9 with older versions of polymake. @@ -69,6 +72,11 @@ there is nothing to worry about. "user" to honour ~/.polymake/settings instead; this is also considerably faster, as polymake then caches its autoconfiguration between calls. + PolymakePersistent + whether one polymake process serves the whole session, rather than + starting a new one for every call. true by default; starting + polymake takes the best part of a second, so this makes any session that + calls polymake more than once considerably faster. PolymakePreferences a list of polymake rule preferences applied to every call, for example [ "*.convex_hull cdd" ] to choose a convex hull diff --git a/lib/environment.gi b/lib/environment.gi index 132f30d..a4a2e97 100644 --- a/lib/environment.gi +++ b/lib/environment.gi @@ -61,6 +61,156 @@ end); ## to a file: polymake's own chatter goes to stderr, so a result on stdout could ## never be trusted. ## +## +## The persistent polymake. Starting polymake costs about 0.8s, almost all of it +## loading the rules of an application, and a session usually makes many calls; +## one long lived process answers them in about a millisecond each. Results +## still go to a file, so the pipe only ever carries a marker line. +## +BindGlobal("POLYMAKING_DONE", "__polymaking_done__"); + +# perl double quoted string literal +BindGlobal("POLYMAKING_PerlString", function(str) + local out, c; + out := "\""; + for c in str do + if c in "\\\"$@" then + Add(out, '\\'); + fi; + Add(out, c); + od; + Add(out, '\"'); + return out; +end); + +BindGlobal("POLYMAKING_Bool", b -> String(Number([b], x -> x = true))); + +BindGlobal("POLYMAKING_PerlList", + l -> Concatenation("[", JoinStringsWithSeparator( + List(l, POLYMAKING_PerlString), ","), "]")); + + +# Read until polymake reports the call finished. fail means the process died, +# in which case the caller retries once with a fresh one. +BindGlobal("POLYMAKING_AwaitDone", function(stream) + local line; + while true do + line := ReadLine(stream); + if line = fail then + return false; + fi; + line := Chomp(line); + if line = POLYMAKING_DONE then + return true; + elif line <> "" then + Info(InfoPolymaking, 2, line); + fi; + od; +end); + + +BindGlobal("POLYMAKING_StopServer", function() + if POLYMAKING_STATE.server <> fail then + if not IsClosedStream(POLYMAKING_STATE.server) then + CloseStream(POLYMAKING_STATE.server); + fi; + POLYMAKING_STATE.server := fail; + fi; +end); + + +BindGlobal("POLYMAKING_StartServer", function() + local cmd, stream, prelude; + + cmd := PolymakeCommand(); + if cmd = fail then + return fail; + fi; + stream := InputOutputLocalProcess(POLYMAKING_TempDirectory("scratch"), cmd, + ["--config-path", + UserPreference("polymaking", "PolymakeConfigPath"), "-"]); + if stream = fail then + return fail; + fi; + + prelude := Concatenation( + "do ", POLYMAKING_PerlString( + Filename(DirectoriesPackageLibrary("polymaking"), "pm.pl")), "; ", + "polymaking_setup(", + POLYMAKING_PerlString(POLYMAKING_ScratchFile("stderr.txt")), ", ", + POLYMAKING_Bool(UserPreference("polymaking", "PolymakeQuiet")), "); ", + "print ", POLYMAKING_PerlString(POLYMAKING_DONE), ", \"\\n\";"); + + if WriteLine(stream, prelude) = fail + or not POLYMAKING_AwaitDone(stream) then + CloseStream(stream); + return fail; + fi; + POLYMAKING_STATE.server := stream; + return stream; +end); + + +BindGlobal("POLYMAKING_Result", function(resfile, errfile, status) + local err, res; + err := StringFile(errfile); + if err = fail then + err := ""; + fi; + if err <> "" then + Info(InfoPolymaking, 2, Chomp(err)); + fi; + res := StringFile(resfile); + if res = fail then + return rec(status := status, stderr := err, result := fail); + fi; + return rec(status := status, stderr := err, result := JsonStringToGap(res)); +end); + + +# Ask the persistent polymake; fail means it could not be used at all, so the +# caller falls back to starting polymake for this one call. +BindGlobal("POLYMAKING_RunServer", function(objfile, keywords, resfile) + local try, stream, call; + + if UserPreference("polymaking", "PolymakePersistent") <> true then + return fail; + fi; + + call := Concatenation( + "polymaking_eval(", POLYMAKING_PerlString(resfile), ", ", + POLYMAKING_PerlString(objfile), ", ", + POLYMAKING_PerlList(UserPreference("polymaking", "PolymakePreferences")), + Concatenation(List(keywords, k -> Concatenation(", ", POLYMAKING_PerlString(k)))), + "); print ", POLYMAKING_PerlString(POLYMAKING_DONE), ", \"\\n\";"); + + # one retry, in case polymake died or was closed since the last call. Talking + # to a closed stream raises an error rather than returning fail, so the whole + # exchange goes through CALL_WITH_CATCH. + for try in [1, 2] do + stream := POLYMAKING_STATE.server; + if stream <> fail and IsClosedStream(stream) then + POLYMAKING_STATE.server := fail; + stream := fail; + fi; + if stream = fail then + stream := POLYMAKING_StartServer(); + if stream = fail then + return fail; + fi; + fi; + if CALL_WITH_CATCH(function() + return WriteLine(stream, call) <> fail + and POLYMAKING_AwaitDone(stream); + end, []) = [true, true] then + return true; + fi; + POLYMAKING_StopServer(); + od; + return fail; +end); + + InstallGlobalFunction(POLYMAKING_Run, function(dir, args) local cmd, errfile, resfile, scriptarg, p, out, status, err, res; @@ -76,6 +226,12 @@ InstallGlobalFunction(POLYMAKING_Run, function(dir, args) RemoveFile(errfile); RemoveFile(resfile); + # args is [objfile, keyword...], or ["--version"] + if Length(args) > 1 and POLYMAKING_RunServer(args[1], args{[2..Length(args)]}, + resfile) = true then + return POLYMAKING_Result(resfile, errfile, 0); + fi; + scriptarg := ["--config-path", UserPreference("polymaking","PolymakeConfigPath"), "--script", Filename(DirectoriesPackageLibrary("polymaking"), "pm.pl"), "--stderr", errfile]; @@ -92,19 +248,7 @@ InstallGlobalFunction(POLYMAKING_Run, function(dir, args) status := Process(dir, cmd, InputTextNone(), out, scriptarg); CloseStream(out); - err := StringFile(errfile); - if err = fail then - err := ""; - fi; - if err <> "" then - Info(InfoPolymaking, 2, Chomp(err)); - fi; - - res := StringFile(resfile); - if res = fail then - return rec(status := status, stderr := err, result := fail); - fi; - return rec(status := status, stderr := err, result := JsonStringToGap(res)); + return POLYMAKING_Result(resfile, errfile, status); end); @@ -164,6 +308,13 @@ end); POLYMAKING_UpdateLegacyGlobals(); +# a stream does not survive into another session +CallAndInstallPostRestore(function() + POLYMAKING_STATE.server := fail; +end); + +InstallAtExit(POLYMAKING_StopServer); + # the data directory a restored workspace names is gone, see issue #17 CallAndInstallPostRestore(POLYMAKING_UpdateLegacyGlobals); diff --git a/lib/pm.pl b/lib/pm.pl index 48d74c2..d13a316 100644 --- a/lib/pm.pl +++ b/lib/pm.pl @@ -1,67 +1,88 @@ # Evaluate polymake properties and write them to a JSON file for polymaking. # -# usage: pm.pl [OPTIONS] RESULTFILE OBJFILE KEYWORD... -# pm.pl [OPTIONS] RESULTFILE --version +# one shot: pm.pl [OPTIONS] RESULTFILE OBJFILE KEYWORD... +# pm.pl [OPTIONS] RESULTFILE --version +# persistent: do "pm.pl"; then call polymaking_setup and polymaking_eval, +# see POLYMAKING_StartServer in lib/environment.gi # # The result is a JSON object with the keys "version", "values" (keyword -> # serialized value), "errors" (keyword -> message) and, if the object could not # be loaded at all, "fatal". Writing it to a file rather than to stdout keeps it # clear of anything polymake prints. -my ($errfile, $quiet, @prefer); -while (@ARGV && $ARGV[0] =~ /^--/ && $ARGV[0] ne '--version') { - my $opt = shift(@ARGV); - last if $opt eq '--'; - if ($opt eq '--stderr') { $errfile = shift(@ARGV) } - elsif ($opt eq '--quiet') { $quiet = 1 } - elsif ($opt eq '--prefer') { push @prefer, shift(@ARGV) } - else { die "pm.pl: unknown option $opt\n" } -} - # Reassociating the glob also catches err_print/warn_print, which write to -# $Polymake::console, and polymake's own fatal error handler. -if (defined $errfile) { - open(STDERR, '>', $errfile) or die "cannot redirect stderr to $errfile: $!\n"; - STDERR->autoflush; +# $Polymake::console, and polymake's own fatal error handler. Verbose must be +# set before any load(), which consults Verbose::files. +sub polymaking_setup { + my ($errfile, $quiet) = @_; + if (defined($errfile) && length($errfile)) { + open(STDERR, '>', $errfile) or die "cannot redirect stderr to $errfile: $!\n"; + STDERR->autoflush; + } + if ($quiet) { + $Polymake::User::Verbose::credits = 0; + $Polymake::User::Verbose::files = 0; + } } -# Must happen before load(), which consults Verbose::files. -if ($quiet) { - $Polymake::User::Verbose::credits = 0; - $Polymake::User::Verbose::files = 0; -} +our %polymaking_applied; -my $out = shift(@ARGV); -my $file = shift(@ARGV); -my %r = (version => "$Polymake::Version", values => {}, errors => {}); +sub polymaking_eval { + my ($out, $file, $prefer, @keywords) = @_; + my %r = (version => "$Polymake::Version", values => {}, errors => {}); -if (defined($file) && $file ne '--version') { - my $obj = eval { load($file) }; - if ($@) { - $r{fatal} = "$@"; - } else { - # Not Polymake::User::prefer_now: under --script $Polymake::User::application - # is a stub whose preferences are unset. Mode::create rather than the usual - # Mode::strict, so polymake does not consider its settings changed and - # rewrite them when a config path is in use. - $obj->type->application->prefs - ->add_preference($_, Polymake::Core::Preference::Mode::create) - for @prefer; + if (defined($file) && length($file)) { + my $obj = eval { Polymake::User::load($file) }; + if ($@) { + $r{fatal} = "$@"; + } else { + # Not Polymake::User::prefer_now: under --script + # $Polymake::User::application is a stub whose preferences are unset. + # Mode::create rather than the usual Mode::strict, so polymake does not + # consider its settings changed and rewrite them when a config path is in + # use. Once per application: in a persistent process, adding the same + # preference again makes polymake complain that one is already in effect. + my $app = $obj->type->application; + for my $expr (@$prefer) { + next if $polymaking_applied{$app->name}{$expr}++; + $app->prefs->add_preference($expr, Polymake::Core::Preference::Mode::create); + } - for my $kw (@ARGV) { - my $v = eval { my $x = $obj; $x = $x->$_ for split /\./, $kw; $x }; - if ($@) { - $r{errors}{$kw} = "$@"; - } elsif (!defined($v)) { - $r{errors}{$kw} = "undefined"; - } else { - my $s = eval { Polymake::Core::Serializer::serialize($v) }; - $@ ? ($r{errors}{$kw} = "$@") : ($r{values}{$kw} = $s); + for my $kw (@keywords) { + my $v = eval { my $x = $obj; $x = $x->$_ for split /\./, $kw; $x }; + if ($@) { + $r{errors}{$kw} = "$@"; + } elsif (!defined($v)) { + $r{errors}{$kw} = "undefined"; + } else { + my $s = eval { Polymake::Core::Serializer::serialize($v) }; + $@ ? ($r{errors}{$kw} = "$@") : ($r{values}{$kw} = $s); + } } } } + + open(my $fh, '>', $out) or die "pm.pl: cannot write $out: $!\n"; + print $fh Polymake::encode_json(\%r); + close($fh); + return 1; +} + +if (@ARGV) { + my ($errfile, $quiet, @prefer); + while (@ARGV && $ARGV[0] =~ /^--/ && $ARGV[0] ne '--version') { + my $opt = shift(@ARGV); + last if $opt eq '--'; + if ($opt eq '--stderr') { $errfile = shift(@ARGV) } + elsif ($opt eq '--quiet') { $quiet = 1 } + elsif ($opt eq '--prefer') { push @prefer, shift(@ARGV) } + else { die "pm.pl: unknown option $opt\n" } + } + polymaking_setup($errfile, $quiet); + my $out = shift(@ARGV); + my $file = shift(@ARGV); + $file = undef if defined($file) && $file eq '--version'; + polymaking_eval($out, $file, \@prefer, @ARGV); } -open(my $fh, '>', $out) or die "pm.pl: cannot write $out: $!\n"; -print $fh Polymake::encode_json(\%r); -close($fh); +1; diff --git a/lib/userpref.gi b/lib/userpref.gi index 15f24f0..8e300b6 100644 --- a/lib/userpref.gi +++ b/lib/userpref.gi @@ -20,7 +20,7 @@ BindGlobal("POLYMAKING_LEGACY_SET", # Temporary directories are created on demand and re-created whenever they have # vanished, e.g. after restoring a workspace saved in an earlier session. BindGlobal("POLYMAKING_STATE", - rec(tmpdir := fail, scratch := fail, + rec(tmpdir := fail, scratch := fail, server := fail, version := fail, versionChecked := false)); @@ -211,3 +211,21 @@ Each entry is passed to polymake's prefer_now. This works even when default := [], check := x -> IsList(x) and ForAll(x, IsString) )); + + +DeclareUserPreference(rec( + package := "polymaking", + name := "PolymakePersistent", + description := [ +"""controls whether one polymake process serves the whole &GAP; session. + +Starting polymake costs the best part of a second, nearly all of it spent +loading the rules of an application, so keeping one process alive makes any +session that calls polymake more than once considerably faster. Set this to +false to start polymake afresh for every call, which is slower but keeps +each call fully independent. +"""], + default := true, + values := [ true, false ], + multi := false +)); diff --git a/tst/persistent.tst b/tst/persistent.tst new file mode 100644 index 0000000..22e855e --- /dev/null +++ b/tst/persistent.tst @@ -0,0 +1,51 @@ +gap> START_TEST("persistent.tst"); +gap> oldpersist := UserPreference("polymaking", "PolymakePersistent");; + +# the same answers either way +gap> results := [];; p := fail;; +gap> for persist in [true, false] do +> SetUserPreference("polymaking", "PolymakePersistent", persist); +> POLYMAKING_StopServer(); +> p := CreatePolymakeObject();; +> AppendPointlistToPolymakeObject(p, [[1/4,1/75],[1/37,1/62],[1/91,1/24],[1/3,1/30]]); +> Add(results, List(["N_VERTICES","VOLUME","VERTICES","FACETS","BOUNDED"], +> kw -> Polymake(p, kw))); +> od; +gap> results[1] = results[2]; +true + +# a server is started on demand, and only when asked for +gap> SetUserPreference("polymaking", "PolymakePersistent", false);; +gap> POLYMAKING_StopServer(); +gap> q := CreatePolymakeObject();; +gap> AppendPointlistToPolymakeObject(q, [[0,0],[1,0],[0,1]]); +gap> Polymake(q, "N_VERTICES"); +3 +gap> POLYMAKING_STATE.server = fail; +true +gap> SetUserPreference("polymaking", "PolymakePersistent", true);; +gap> Polymake(q, "VOLUME" : PolymakeNolookup); +1/2 +gap> POLYMAKING_STATE.server = fail; +false + +# a polymake that has gone away is replaced rather than reported +gap> CloseStream(POLYMAKING_STATE.server); +gap> Polymake(q, "N_VERTICES" : PolymakeNolookup); +3 +gap> POLYMAKING_STATE.server = fail; +false + +# strings reaching perl are quoted, whatever they contain +gap> POLYMAKING_PerlString("a\"b\\c$d@e"); +"\"a\\\"b\\\\c\\$d\\@e\"" +gap> POLYMAKING_PerlList(["x", "y"]); +"[\"x\",\"y\"]" +gap> POLYMAKING_Bool(true); +"1" +gap> POLYMAKING_Bool(false); +"0" + +# +gap> SetUserPreference("polymaking", "PolymakePersistent", oldpersist);; +gap> STOP_TEST("persistent.tst", 1); From 89615a1383c8753da7cbcca79efe8928e3429492 Mon Sep 17 00:00:00 2001 From: Max Horn Date: Mon, 17 Aug 2026 09:52:37 +0200 Subject: [PATCH 2/3] Fix rule preferences under --script polymake compiles a --script file with its own namespace pragma, which rejects `our`, so the hash tracking which preferences have been applied blew up with reference to an undeclared variable %polymaking_applied A file scoped lexical works in both modes. Reading the script into the persistent process happens without that pragma, which is why only the one-shot path was affected, and only when a rule preference was actually set -- so the CI convex hull backend jobs caught it and everything else passed. Exercise preferences in both modes from tst/persistent.tst, so the default job covers this rather than only the backend matrix. Co-Authored-By: Claude Opus 5 --- lib/pm.pl | 4 +++- tst/persistent.tst | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/pm.pl b/lib/pm.pl index d13a316..dd9b5f4 100644 --- a/lib/pm.pl +++ b/lib/pm.pl @@ -25,7 +25,9 @@ sub polymaking_setup { } } -our %polymaking_applied; +# lexical, not a package variable: under --script polymake compiles this with +# its own namespace pragma, which rejects `our`. +my %polymaking_applied; sub polymaking_eval { my ($out, $file, $prefer, @keywords) = @_; diff --git a/tst/persistent.tst b/tst/persistent.tst index 22e855e..18a2d11 100644 --- a/tst/persistent.tst +++ b/tst/persistent.tst @@ -14,6 +14,23 @@ gap> for persist in [true, false] do gap> results[1] = results[2]; true +# rule preferences reach polymake in both modes. polymake compiles the helper +# script differently under --script than when the persistent process reads it, +# so this needs exercising both ways. +gap> oldprefs := UserPreference("polymaking", "PolymakePreferences");; +gap> SetUserPreference("polymaking", "PolymakePreferences", +> ["*.convex_hull beneath_beyond"]);; +gap> prefres := [];; +gap> for persist in [true, false] do +> SetUserPreference("polymaking", "PolymakePersistent", persist); +> POLYMAKING_StopServer(); +> Add(prefres, Polymake(p, "N_VERTICES" : PolymakeNolookup)); +> od; +gap> prefres; +[ 4, 4 ] +gap> SetUserPreference("polymaking", "PolymakePreferences", oldprefs);; +gap> POLYMAKING_StopServer(); + # a server is started on demand, and only when asked for gap> SetUserPreference("polymaking", "PolymakePersistent", false);; gap> POLYMAKING_StopServer(); From c18663cd7935cff88b6b864b7aa42e3ed24e67f3 Mon Sep 17 00:00:00 2001 From: Max Horn Date: Mon, 17 Aug 2026 10:01:47 +0200 Subject: [PATCH 3/3] Restart polymake when the settings it was started with change The config path and quiet flag are fixed when a polymake process starts, and a rule preference cannot be withdrawn once applied, so with a persistent process changing any of them had no effect until something else happened to restart it. Setting PolymakeQuiet to false mid-session, say, went unnoticed. The settings a process was started with are now recorded, and it is replaced when they no longer match. Also let polymaking_eval reopen the stderr file rather than rely on the handle polymaking_setup established. Every call unlinks that file before running, and a persistent process holds it open, so from the second call on GAP would be reading a path the process no longer writes to. In practice polymake says its piece once per session and nothing is observably lost -- I could not construct a case that shows a difference -- but the reopen makes each call start from a live file rather than depending on that. For the same reason, polymake announces a credit once per session rather than once per call, so with a persistent process these are seen once; noted in the manual and in CHANGES. Co-Authored-By: Claude Opus 5 --- CHANGES.md | 4 +++- lib/environment.gi | 18 ++++++++++++++++++ lib/pm.pl | 9 +++++++-- lib/userpref.gi | 6 +++++- 4 files changed, 33 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 23da442..f8eb0e6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,7 +3,9 @@ - one polymake process now serves the whole GAP session instead of a fresh one being started for every call, which makes a session of 40 calls about ten times faster. Set the new `PolymakePersistent` preference to `false` for the - old behaviour. + old behaviour. Note that polymake announces the third party software it uses + once per session, so those messages now appear once rather than once per + call. - polymake 4.0 or newer is now required, and the GAP package json is a new dependency; json needs GAP 4.12, so that is now polymaking's minimum too. polymaking now writes and reads polymake's own JSON data format diff --git a/lib/environment.gi b/lib/environment.gi index a4a2e97..19fd029 100644 --- a/lib/environment.gi +++ b/lib/environment.gi @@ -119,6 +119,17 @@ BindGlobal("POLYMAKING_StopServer", function() end); +# The settings baked into a running polymake: its config path and quiet flag are +# fixed when it starts, and rule preferences cannot be withdrawn once applied. +# Changing any of them means starting again. +BindGlobal("POLYMAKING_ServerSettings", function() + return [ PolymakeCommand(), + UserPreference("polymaking", "PolymakeConfigPath"), + UserPreference("polymaking", "PolymakeQuiet"), + UserPreference("polymaking", "PolymakePreferences") ]; +end); + + BindGlobal("POLYMAKING_StartServer", function() local cmd, stream, prelude; @@ -147,6 +158,7 @@ BindGlobal("POLYMAKING_StartServer", function() return fail; fi; POLYMAKING_STATE.server := stream; + POLYMAKING_STATE.serverSettings := POLYMAKING_ServerSettings(); return stream; end); @@ -179,6 +191,7 @@ BindGlobal("POLYMAKING_RunServer", function(objfile, keywords, resfile) call := Concatenation( "polymaking_eval(", POLYMAKING_PerlString(resfile), ", ", + POLYMAKING_PerlString(POLYMAKING_ScratchFile("stderr.txt")), ", ", POLYMAKING_PerlString(objfile), ", ", POLYMAKING_PerlList(UserPreference("polymaking", "PolymakePreferences")), Concatenation(List(keywords, k -> Concatenation(", ", POLYMAKING_PerlString(k)))), @@ -187,6 +200,11 @@ BindGlobal("POLYMAKING_RunServer", function(objfile, keywords, resfile) # one retry, in case polymake died or was closed since the last call. Talking # to a closed stream raises an error rather than returning fail, so the whole # exchange goes through CALL_WITH_CATCH. + if POLYMAKING_STATE.server <> fail + and POLYMAKING_STATE.serverSettings <> POLYMAKING_ServerSettings() then + POLYMAKING_StopServer(); + fi; + for try in [1, 2] do stream := POLYMAKING_STATE.server; if stream <> fail and IsClosedStream(stream) then diff --git a/lib/pm.pl b/lib/pm.pl index dd9b5f4..1dbd68f 100644 --- a/lib/pm.pl +++ b/lib/pm.pl @@ -30,9 +30,14 @@ sub polymaking_setup { my %polymaking_applied; sub polymaking_eval { - my ($out, $file, $prefer, @keywords) = @_; + my ($out, $errfile, $file, $prefer, @keywords) = @_; my %r = (version => "$Polymake::Version", values => {}, errors => {}); + # Reopen rather than rely on the handle from polymaking_setup: the caller + # starts each call from a clean file, and a persistent process would otherwise + # go on writing to the old, unlinked one. + polymaking_setup($errfile, 0) if defined($errfile) && length($errfile); + if (defined($file) && length($file)) { my $obj = eval { Polymake::User::load($file) }; if ($@) { @@ -84,7 +89,7 @@ sub polymaking_eval { my $out = shift(@ARGV); my $file = shift(@ARGV); $file = undef if defined($file) && $file eq '--version'; - polymaking_eval($out, $file, \@prefer, @ARGV); + polymaking_eval($out, undef, $file, \@prefer, @ARGV); } 1; diff --git a/lib/userpref.gi b/lib/userpref.gi index 8e300b6..b221693 100644 --- a/lib/userpref.gi +++ b/lib/userpref.gi @@ -20,7 +20,8 @@ BindGlobal("POLYMAKING_LEGACY_SET", # Temporary directories are created on demand and re-created whenever they have # vanished, e.g. after restoring a workspace saved in an earlier session. BindGlobal("POLYMAKING_STATE", - rec(tmpdir := fail, scratch := fail, server := fail, + rec(tmpdir := fail, scratch := fail, + server := fail, serverSettings := fail, version := fail, versionChecked := false)); @@ -174,6 +175,9 @@ and are usually just noise, so they are turned off by default. Set this to Independently of this, everything polymake writes to standard error is shown at InfoPolymaking level 2, and is included in the error message and in POLYMAKE&uscore;LAST&uscore;FAIL&uscore;REASON when a call fails. +

+polymake reports a credit once per session rather than once per call, so with +PolymakePersistent set, which is the default, each is seen once. """], default := true, values := [ true, false ],