JavaFX Canvas 踩 GPU 纹理上限 视口裁剪方案
现象有条超长的层位线(20000+ 道)在雷达图上只显示了一截,甚至整段空白;短一点的层位线倒是正常。
原因原来的渲染是把整段层位线一口气画到一个 Canvas 上,Canvas 的宽度就等于这段的道数。一条 20000 道的层位线,Canvas 宽度起步就是 20000 像素,缩放之后还更高。
而 GPU 纹理是有尺寸上限的,一般在 16384 ~ 32768 像素之间,看显卡。Canvas 宽度一旦超过这个上限,渲染就直接失败——而且是静默失败,表现就是空白或者只画了一截,啥报错都没有,排查起来很费劲。
视口裁剪办法就是只画当前能看见的那一截,整段别都画。Canvas 宽度始终差不多等于视口宽度(屏幕上能看到的范围),内容跟着滚动、缩放重绘:
根据当前的滚动位置和缩放,算出视口覆盖的道号区间 [from, to]。
只把这个区间里的层位线点画到 Canvas 上。
Canvas 宽度固定成视口宽度,永远不会越过 GPU 的纹理上限。
用户一滚动或者缩放,就重新算区间、重绘。
附带的优化视口裁剪顺带把性能问题也解决了:以前不管你看哪,都在画 20000 个点,现在只画视口里那几百 ...
SQLite 批量更新 CASE WHEN 与 999 变量上限
痛点病害的”共有 / 复合”状态算完得持久化。几百上千条病害,最开始是逐条 UPDATE,两个毛病:一是慢,每次 UPDATE 都是一次磁盘事务;二是没有整体事务保护,中途一旦失败,就留下一堆半新半旧的数据。
CASE WHEN 批量更新把 N 条逐条 UPDATE 合成一条,用 CASE WHEN 给每行不同的值:
UPDATE defectSET compound_level = CASE id WHEN 101 THEN 'AA' WHEN 102 THEN 'A1' WHEN 103 THEN 'B' END, is_compound = CASE id WHEN 101 THEN 1 WHEN 102 THEN 1 WHEN 103 THEN 0 ENDWHERE id IN (101, 102, 103)
一趟往返就更新了多行多列,磁盘事务次数从 N 次降到 1 次。外面再用 TransactionTe ...
用事件总线解耦模块 零线变更的级联重算
问题用户调一下”零线”,后面会牵一发动全身:受影响的文件得重算厚度不足段、层位段平均深度、病害深度,层位识别和病害标注那边的雷达图、列表也都得跟着刷新。
要是让零线模块直接去调各模块的刷新方法,耦合就焊死了——零线模块得记着谁要刷新、调谁的哪个方法,以后多加个模块还得回来改零线的代码。
事件总线加一个轻量的事件总线,零线模块只管”发事件”,谁在意谁自己”订阅”:
// 发布方:零线模块,不关心谁来处理eventBus.publish(ZeroLineChangedEvent(affectedFileIds))// 订阅方:层位识别模块,自己决定怎么响应@Subscribefun onZeroLineChanged(e: ZeroLineChangedEvent) = scope.launch { recalcLayerSegments(e.affectedFileIds) refreshChart()}
好处其实就是几件事搅一块儿:发布方和订阅方互相不认识,以后加模块订阅一下就行,零线代码不用动;重算(耗时)和刷新(UI)各跑各的协程,互不卡;零线模块 ...
用并查集识别隧道相邻测线的共有病害
业务场景隧道里有多条平行的测线,相邻两条(编号差 1)上要是各有一个病害、里程范围还重叠,那很可能就是同一个病害在两条测线上各露了一面——这种得标成”共有病害”。
难点在传递性判断这事不能光看两两配对,因为重叠是会传递的:A 和 B 重叠、B 又和 C 重叠,那 A、B、C 其实是同一组,该归到一块儿。要是只两两标记,A-B 一组、B-C 一组,C 跟 A 之间的关系就丢了,分出来的组是碎的。
并查集(Union-Find)这种”沾边就连、要整体分组”的活,正好是并查集擅长的:
class UnionFind(n: Int) { val parent = IntArray(n) { it } val rank = IntArray(n) fun find(x: Int): Int { if (parent[x] != x) parent[x] = find(parent[x]) // 路径压缩 return parent[x] } fun union(a: Int, b: I ...
Excel 导入的两个坑 字段截断与 32767 字符限制
背景做历史层位 / 病害的 Excel 导入导出的时候,连踩了两个”长度上限”的坑,一个在数据库,一个在 Excel 本身,记一下。
坑一:SQLite 字段截断导入历史层位线以后,滚到中间位置,长段的层位线突然变成了一条水平直线。排查发现,存深度数据的 depth_index_array 列定义成了 VARCHAR(5000),超长段的数据被静默截断了——SQLite 写入的时候不报错,读出来就是一截残缺数据,渲染自然就错了。
解决办法:把这列改成 TEXT,不限长度。SQLite 的 TEXT 本身不限长,VARCHAR(N) 里的 N 只是个 hint、并不会真限制(SQLite 弱类型),但有些驱动 / ORM 层会按声明长度截断,所以声明成 TEXT 最保险。
教训就是:别拿 VARCHAR 存可能很长的内容,长文本一律 TEXT,免得被中间某一层按声明长度给截了。
坑二:Excel 单元格字符上限层位导出的时候,原始数据那列把整个深度数组塞进了一个单元格,结果导出直接失败。原因是 Excel 对单个单元格的字符数有硬上限:32767 个字符。深度数组动不 ...
JavaFX 截图卡死 UI 的根治 Java2D 纯数据渲染
痛点病害导出的时候,每条病害要生成一张雷达图截图。原来的做法是直接对着界面上那个 JavaFX 节点调 snapshot() 截图。问题来了:snapshot() 必须在 FX Application Thread 上跑,导出几十上百张图的时候,主线程被这些截图任务占得死死的,整个 UI 完全卡住,进度条都不带动的。
思路卡死的根子在于渲染绑死在 UI 线程上了。要彻底解决,就得让渲染脱离 JavaFX 节点,变成在随便哪个线程上都能跑的纯计算。所以我把截图渲染从”拍 UI”改成了”用 Java2D 拿纯数据画”。
改造按 MVVM 三层切开:
View 层只管从 UI 组件里抠出纯数据(当前视口的里程范围、采样值数组、增益、颜色映射这些),不掺和画图。
ViewModel 层负责渲染的调度和参数拼装,把 View 抠出来的数据组装成绘图参数。
Service 层是纯绘图,用 BufferedImage + Graphics2D 画雷达图、层位线、病害框、信息面板,完全不碰 JavaFX。
// Service 层,任意线程可跑fun render(params: RenderPa ...
批量导出性能优化 消除 N+1 查询与协程并发
问题病害导出 Excel 的时候,每条病害都得查它的类型颜色、所属层位、雷达文件的里程数组……最早是”遍历病害、一条一条查库”,导出几百上千条的时候,查库次数跟着病害数线性往上涨——典型的 N+1。批量导出耗时的大头全花在查库上,反而不是绘图。
优化分三步走:
批量预查询,干掉 N+1
把循环里那条单查,改成开头一次性批量查、构个 Map 缓起来:
val colorMap = service.batchQueryColors(defects.map { it.typeId })defects.forEach { d -> d.color = colorMap[d.typeId] } // O(1) 查
像病害类型颜色这种”N 条病害其实只有几种类型”的数据,批量查一次,查询次数从 N 降到 1。
按雷达文件分组,共享渲染上下文
原来每条病害导出截图都要重新打开雷达文件、重算里程数组。改成按雷达文件分组,同一个文件的病害共享一次”开文件 + 算里程数组”的上下文,省掉一堆重复 IO。
协程并发各文件
分完组,各雷达文件的导出互相不依赖 ...
JavaFX 后台线程操作 ObservableList 报 IllegalStateException
现象做”导入历史数据”功能的时候,解析 Excel 是个耗时操作,很自然就丢到后台线程。解析完直接往绑了 TableView 的 ObservableList 里 add,结果运行就炸了:
Exception in thread "pool-1-thread-1" java.lang.IllegalStateException: Not on FX application thread; currentThread = pool-1-thread-1
原因JavaFX 有条规矩:所有对 Scene Graph 的改动(包括 ObservableList、Property 的变更)都得在 JavaFX Application Thread 上做。ObservableList 一变就会触发 UI 监听器去刷表格,后台线程改它就破坏了线程安全,所以框架直接抛异常拦下来。
一开始图省事的写法最开始图省事,每解析一条就用 Platform.runLater 包一下 add:
thread { val parsed = parseRow(row) ...
用 Kotlin 协程替代 Thread 并行加载多测线雷达图
背景多测线雷达图加载,原来用 ExecutorService + Thread:一条测线一个任务,结果靠回调或者 Future 收。线程数基本靠拍脑袋,取消靠一个 volatile 标志位,异常处理各写各的,并行度也把握不好。串行加载几条测线的时候界面就干等着,体感很慢。
换成协程换成 Kotlin 协程以后,多测线并行加载就剩这么一段:
suspend fun loadMultiScanLines(files: List<RadarFile>): List<RadarData> = coroutineScope { files.map { file -> async(Dispatchers.IO) { readRadar(file) } // 每条测线一个异步任务 }.awaitAll() // 并行,全部完成后返回}
比 Thread 那套舒服的地方:所有 async 任务都挂在同一个 co ...
MVVM 重构 把数据加载从 View 下沉到 ViewModel
起因项目最早的写法是典型的”胖 Controller”:View(FXML 对应的 Controller)里既管 UI 交互,又直接调 Service 加载数据、刷表格、维护选中状态。功能一多,单个 Controller 几千行,加载数据的逻辑和 UI 操作揉一块儿,几乎没法单测,改一处怕动全身。
拆分按 MVVM 把职责切开:
View 只管 UI 绑定和用户交互,从 UI 组件里抠纯数据(比如表格当前选中的行),不含业务。
ViewModel 持有状态(Property / ObservableList),负责数据加载的调度和参数构建,对 View 暴露可绑定的属性。
Service 是纯逻辑,不碰 JavaFX,能在任意线程跑、能单测。
核心动作就一个:把数据加载从 View 剥到 ViewModel。原来 Controller 里 service.load() 之后跟一大串 UI 刷新,现在改成 ViewModel 加载完更新自己的 ObservableList,View 靠绑定自动刷新:
class RadarViewModel { val fi ...
