Release MaxMath 1.0.1 performance optimizations - #3
Merged
Conversation
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
marked this pull request as ready for review
August 7, 2026 13:33
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
assembleDebugCI 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 / 验证
origin/main.git diff --check origin/main..HEADpassed.1151432CD7742DAAEDDB577A91E251D07BEE6EAC5158FE0B6EB6FECDC0659F3B.assembleDebugfor this PR.