Skip to content

Release MaxMath 1.0.1 performance optimizations - #3

Merged
ParuhParhat merged 6 commits into
mainfrom
perf/optimization-pass
Aug 7, 2026
Merged

Release MaxMath 1.0.1 performance optimizations#3
ParuhParhat merged 6 commits into
mainfrom
perf/optimization-pass

Conversation

@ParuhParhat

Copy link
Copy Markdown
Owner

Summary / 摘要

Release MaxMath 1.0.1 with a smaller runtime package, a reusable Maxima subprocess, lower-allocation 2D/3D plotting paths, and stronger build checks.

发布 MaxMath 1.0.1:缩减运行时包体、复用 Maxima 子进程、降低 2D/3D 绘图热路径分配,并加强构建检查。

What changed / 改动

  • Removed duplicate and documentation-only engine assets; the installer now rebuilds the extracted runtime directory.

  • Reuses one Maxima child process between clean requests, with sentinel completion, state reset, timeout/cancellation recovery, and idle cleanup.

  • Reworked curve sampling, painter paths, GL meshes, and renderer state to reuse buffers and avoid per-frame boxing/allocation.

  • Combined plot probes, moved bitmap decoding to IO, enabled Gradle caching/parallelism, and added an assembleDebug CI compile check.

  • Bumped the app to version 1.0.1 (versionCode 15), added optimization/release documentation, and updated both README download links.

  • 移除重复及仅用于文档/链接期的引擎资产,安装器改为整目录重建。

  • 在正常请求之间复用 Maxima 子进程,并加入完成哨兵、状态复位、超时/取消恢复与空闲清理。

  • 复用曲线采样、画笔路径、GL 网格和渲染器缓冲,减少逐帧装箱与分配。

  • 合并绘图探测、将图像解码移至 IO、启用 Gradle 缓存/并行,并在 CI 中加入 assembleDebug

  • 版本更新为 1.0.1(versionCode 15),补充优化/发布文档,并同步中英文 README 下载链接。

Why / 原因

The previous package contained duplicated native/runtime files, each calculation paid a Maxima process startup cost, and plotting created avoidable hot-path allocations. These changes reduce APK/runtime footprint and improve repeated calculations and interactive rendering.

旧版本包含重复的原生/运行时文件,每次计算都承担 Maxima 进程启动开销,绘图热路径也存在可避免的对象分配。本次更新针对包体、重复计算延迟和交互渲染性能进行优化。

Validation / 验证

  • Clean worktree; branch is based on current origin/main.
  • git diff --check origin/main..HEAD passed.
  • Release APK ZIP structure checked: manifest, DEX, arm64 native libraries, and engine assets are present; no other ABI is bundled.
  • APK SHA-256: 1151432CD7742DAAEDDB577A91E251D07BEE6EAC5158FE0B6EB6FECDC0659F3B.
  • GitHub Actions is expected to run unit tests and assembleDebug for this PR.

yueye6811 and others added 6 commits August 7, 2026 14:14
assets/engine was 67MB, of which 16.3MB was byte-identical to files
already shipped in jniLibs: lib/maxima/5.49.0/binary-ecl/maxima matches
libmaxima.so, and lib/libecl.so matches libecl.so (md5 584ba60b / same
size). Both cost space twice - once in the APK, once extracted into
filesDir - and neither can ever run, since Android 10+ forbids execve
from filesDir. That is why the binary is shipped via jniLibs at all.

The stale copies persisted because package-engine.sh only ever did
incremental cp and never cleaned its target, so files dropped from the
packaging step (libecl.so, and additions/qepcad from the legacy MoA
script) stayed in the working tree forever and shipped anyway. Clean
the target first, and prune share-tree documentation (PDF manuals alone
were 9.3MB) plus ECL link-time archives.

Also move init.lisp.template generation into package-engine.sh - only
package-moa-engine.sh produced it, so cleaning the target would
otherwise leave the engine without its init template.

EngineInstaller drops the assets fallbacks for the executable and for
libecl.so; on a real device those paths cannot work, and keeping them
turned a packaging error into an opaque execve errno later. Missing
binaries now fail immediately with the expected path. Extraction also
rebuilds the directory instead of overwriting it, so upgrading installs
shed content that is no longer packaged, and copies stream instead of
buffering whole files.

assets/engine: 67MB -> 29MB.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Plot2DPainter.draw runs from a Compose Canvas on every gesture frame
during live 2D pan/zoom. Each frame it allocated a Paint per curve, a
Path per segment, an AndroidPaint in drawTickLabels plus another per
annotation label, a List<List<Pair<Double,Double>>> of boxed samples,
and a String.format per tick label. Paints, paths, the sample buffer
and the formatted tick labels are now reused; ticks recompute only when
the range actually changes.

CurveSampler grows an array-based entry point (interleaved coordinates
plus segment-boundary indices) so sampling no longer boxes. The
List<Pair> API stays as a thin adapter over it, keeping CurveSamplerTest
as a check on the new logic rather than a parallel implementation.

PlotGlMesh allocated a boxed four-element list per grid cell to test for
finite corners - 14161 of them in buildSurface and 141610 in
buildContourLines, where the check sits inside the level loop - plus a
boxed Int list for indices, a boxed Float list for contour vertices, an
IntArray per cell in accumulateNormal, and a fresh FloatArray per grid
point from viridis. All replaced with preallocated arrays and direct
comparisons.

PlotGlRenderer queried glGetUniformLocation/glGetAttribLocation for the
axes program on every frame while the main program cached its locations
at link time; cache them the same way and hoist the constant colours and
light vector.

PlotScreen decoded the plot PNG with BitmapFactory.decodeFile inside
remember, on the composition thread. Moved to produceState on
Dispatchers.IO. No subsampling: matplotlib emits 720x480, and the view
allows up to 8x zoom.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every request forked a fresh maxima --batch and reloaded a 12MB ECL
image, typically 1-3s on a phone. The comment in maxmath_engine.cpp
explained why: completion was signalled by process exit because a stdout
sentinel "may never arrive on Android due to unflushed buffering".

That is fixable directly. init.lisp is ours, so define a Maxima-callable
$maxmath_done that prints a sentinel and calls finish-output - a blocking
flush, which is exactly the guarantee that was missing. Results already
came from the with_stdout file rather than stdout (see ScriptRunner), so
only the completion signal needed solving.

The native layer now keeps a warm child with stdin as a pipe, writes
kill(all) + batch(script) + maxmath_done() per request, and reads stdout
until the sentinel. kill(all) matters because a persistent process would
otherwise leak state between requests - one "x: 5" in raw mode would
follow the user around.

Reuse invariant: a child returns to the warm pool only if its sentinel
arrived cleanly. Timeout, cancel, EOF and unexpected exit all kill and
respawn lazily. This is what makes an interactive Maxima prompt (asksign,
askinteger) safe - it would otherwise swallow the next script as its
answer - and it degrades to the previous timeout behaviour instead.
Crash diagnostics (exit code, signal) are still reported on EOF, reaped
before the SIGKILL that would mask them.

Persistence needs the process to outlive one request, so EngineService
gets a startService keep-alive with a 60s idle timeout, which also stops
re-paying Python/Matplotlib startup per plot. MaximaEngine carries its
own idle reaper as well: light operations run in the UI process, which
has no service to bound them.

MaximaEngine.cancel now checks whether a run is actually in flight.
CalcViewModel and PlotViewModel both call cancel unconditionally before
each computation; without the check, the idle warm child would be killed
every time and persistence would be defeated.

The 2D annotation probes are merged into one script per function. Zeros
and extrema both start from a solve over the same expression, so they
were two round trips where one does; each side keeps its own errcatch so
an unsolvable zero probe no longer costs the extrema. The default
two-function plot goes from 4 engine round trips to 2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Gradle build cache and parallel execution: the three modules have no
serial dependency between them. Configuration cache is left commented
out - Chaquopy 17 compatibility is unverified here, and a broken build
is worse than a slower one.

CI ran unit tests only, which compile test sources but never the Compose
and GL code, so a UI-side compile error could reach main. Add
assembleDebug as a compile check, and lint as a non-blocking step -
lint has never run on this repo, so make its findings visible without
gating on an unknown backlog.

No APK size step: engine runtime assets are gitignored, so CI cannot
build a representative APK. The size regression guard lives where it can
actually run - assertions in EngineInstallerTest that the duplicate
binaries and PDFs are absent, plus the checks in native/README.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bump to 1.0.1 (versionCode 15) and document the optimization pass
(packaging slim-down, persistent Maxima, plot hot-path de-boxing,
build/CI improvements).
@ParuhParhat
ParuhParhat marked this pull request as ready for review August 7, 2026 13:33
@ParuhParhat
ParuhParhat merged commit d477905 into main Aug 7, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant