pagemap.go 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. package main
  2. import (
  3. "encoding/base64"
  4. "io"
  5. "net/http"
  6. "strconv"
  7. "sync"
  8. )
  9. // ===== 页映射紧凑索引 (分块懒加载的数据源) =====
  10. //
  11. // 旧实现 (rPages) 每被调用一次就要:
  12. // 1. 同步等全库审计算完 (要扫 19.5 万页);
  13. // 2. 逐页 readPage, 造 19.5 万个 PageItem 结构体;
  14. // 3. 再造两张 19.5 万键的 map (role/owner, 键还是页号的十进制字符串);
  15. // 4. 把这三样一次性 json 序列化 —— 单次响应几十 MB。
  16. //
  17. // 而概览页 / 堆链页 / 空闲页三个视图各会调一次 /api/pages, 前端拿到后还要把
  18. // 19.5 万个 <div> 拼成一个巨型字符串塞进 innerHTML, 于是浏览器卡死甚至崩溃。
  19. //
  20. // 现在改成: 全库只建一次紧凑索引 —— 每页 1 字节类型 + 1 字节使用率,
  21. // 19.5 万页也只占 ~390KB 内存; 接口按块 (base64 字节串) 返回。
  22. // 前端分块拉取 + canvas 虚拟化渲染, 只画视口内的格子, DOM 里永远只有一块画布。
  23. const (
  24. pmChunkDefault = 20000 // 单块默认页数 (base64 后约 54KB)
  25. pmChunkMax = 65536
  26. pmNotLoaded = -9 // "该页数据还没拉到前端" 的哨兵 (仅前端使用)
  27. )
  28. // pageIndex 全库页映射的紧凑表示。
  29. type pageIndex struct {
  30. total int
  31. typ []int8 // 下标 = 页号 (1..total); 0 号槽不用。取值见 PG_* 常量
  32. usg []uint8 // 使用率百分比 0..100
  33. counts map[string]int
  34. allocated int
  35. neverUsed int
  36. firstFree uint32
  37. }
  38. var (
  39. pmMu sync.Mutex // 保护 pmIdx / pmGen
  40. pmBuildMu sync.Mutex // 串行化构建: 并发请求只算一次
  41. pmIdx *pageIndex
  42. pmGen uint64
  43. )
  44. var pmTypeNames = map[int]string{
  45. 0: "free", 1: "table", 2: "data", 3: "heap", 4: "imeta",
  46. 5: "idata", 6: "overflow", 7: "wal", 8: "sext",
  47. }
  48. // pmKind 归一化页类型: 返回 (给前端着色的类型值, counts 用的键名)。
  49. // 未知类型字节统一折叠成 PG_OTHER, 免得前端拿到没有色号的取值。
  50. // 注意与 audit.go 的 pageTypeName 区分: 那个返回的是 "Free"/"Table" 这类展示名,
  51. // 且不认识本文件这几个哨兵; 这里的键名要和旧 rPages 的 counts 保持一致。
  52. func pmKind(pt int) (int, string) {
  53. switch pt {
  54. case PG_RESERVED:
  55. return pt, "reserved"
  56. case PG_NEVER:
  57. return pt, "never"
  58. case PG_ORPHAN:
  59. return pt, "orphan"
  60. }
  61. if n, ok := pmTypeNames[pt]; ok {
  62. return pt, n
  63. }
  64. return PG_OTHER, "other"
  65. }
  66. func pmNewCounts() map[string]int {
  67. return map[string]int{
  68. "hdr": 0, "table": 0, "data": 0, "heap": 0, "imeta": 0, "idata": 0,
  69. "overflow": 0, "wal": 0, "sext": 0, "free": 0, "never": 0,
  70. "reserved": 0, "orphan": 0, "other": 0,
  71. }
  72. }
  73. // buildPageIndex 单趟扫描全库, 生成紧凑页索引。这是唯一一次整库读取。
  74. //
  75. // 刻意**不依赖全库审计**: 页图需要的只有头部 (NextFreePage) 和空闲链, 而这两样
  76. // 在 openH 里就已经就绪 (rHdr + rFreeChain)。审计在大表上要跑一两分钟, 若页图
  77. // 跟着一起等, 用户打开库后半天出不来图 —— 现在解耦后几秒就能出图, 且分块返回。
  78. func buildPageIndex() *pageIndex {
  79. idx := &pageIndex{counts: pmNewCounts()}
  80. numPages := totalPages()
  81. idx.total = numPages
  82. if numPages <= 0 {
  83. return idx
  84. }
  85. // 页号约定与 runAudit 完全一致 (见 audit.go): 0=header, 1=预留, 分配从 2 开始。
  86. nf := le32(hdr[12:16])
  87. idx.firstFree = nf
  88. if nf > 2 {
  89. idx.allocated = int(nf) - 2
  90. }
  91. if idx.allocated > numPages {
  92. idx.allocated = numPages
  93. }
  94. reserved := 1
  95. if idx.neverUsed = numPages - idx.allocated - reserved; idx.neverUsed < 0 {
  96. idx.neverUsed = 0
  97. }
  98. firstFree := nf
  99. if firstFree == 0 {
  100. firstFree = uint32(numPages) + 1
  101. }
  102. freeSet := map[uint32]bool{}
  103. if fi := rFreeChain(); fi != nil {
  104. freeSet = make(map[uint32]bool, fi.Total)
  105. for _, p := range fi.Pages {
  106. freeSet[p] = true
  107. }
  108. }
  109. idx.typ = make([]int8, numPages+1)
  110. idx.usg = make([]uint8, numPages+1)
  111. f, e := dbFile()
  112. if e != nil {
  113. return idx
  114. }
  115. // 顺序批量读: 一次 512 页 (8MB)。逐页 ReadAt 要 17.7 万次系统调用 + 17.7 万次
  116. // 16KB 分配, 光 GC 就够呛; 批量读是连续 I/O, 快一个量级。
  117. const batch = 512
  118. buf := make([]byte, batch*PGSZ)
  119. for base := 1; base <= numPages; base += batch {
  120. n := batch
  121. if base+n-1 > numPages {
  122. n = numPages - base + 1
  123. }
  124. if _, rerr := f.ReadAt(buf[:n*PGSZ], pgOff(uint32(base))); rerr != nil && rerr != io.EOF {
  125. break
  126. }
  127. for k := 0; k < n; k++ {
  128. pid := uint32(base + k)
  129. off := k * PGSZ
  130. us := float64(le32(buf[off+16:off+20])) / float64(PGSZ) * 100
  131. if us > 100 {
  132. us = 100
  133. }
  134. idx.usg[pid] = uint8(us + 0.5)
  135. raw := int(buf[off])
  136. switch {
  137. case pid == 1:
  138. raw = PG_RESERVED // 引擎预留页, 从不分配
  139. case pid >= firstFree:
  140. raw = PG_NEVER // 从未分配过的尾页
  141. case freeSet[pid]:
  142. raw = PG_HEAD // 空闲链上的页
  143. case raw == PG_HEAD:
  144. // 类型字节=0 却不在空闲链上: 已下发区间内的零页 —— 孤儿/泄漏页。
  145. raw = PG_ORPHAN
  146. }
  147. pt, tn := pmKind(raw)
  148. idx.typ[pid] = int8(pt)
  149. idx.counts[tn]++
  150. }
  151. }
  152. return idx
  153. }
  154. // pagemapCached 取页索引 (首次调用时同步构建, 之后命中缓存)。
  155. // 缓存随切库代数 (auditGen) 失效, 避免旧库的结果污染新库。
  156. func pagemapCached() *pageIndex {
  157. cacheMu.Lock()
  158. gen := auditGen
  159. cacheMu.Unlock()
  160. pmMu.Lock()
  161. if pmIdx != nil && pmGen == gen {
  162. idx := pmIdx
  163. pmMu.Unlock()
  164. return idx
  165. }
  166. pmMu.Unlock()
  167. // 构建很慢 (要读满库), 串行化: 并发的调用者排队, 不复算。
  168. pmBuildMu.Lock()
  169. defer pmBuildMu.Unlock()
  170. pmMu.Lock()
  171. if pmIdx != nil && pmGen == gen {
  172. idx := pmIdx
  173. pmMu.Unlock()
  174. return idx
  175. }
  176. pmMu.Unlock()
  177. idx := buildPageIndex()
  178. pmMu.Lock()
  179. pmIdx, pmGen = idx, gen
  180. pmMu.Unlock()
  181. return idx
  182. }
  183. // startPagemapAsync 后台预热页索引 (幂等)。
  184. // 不依赖审计, 所以和审计并行跑; 打开库后几秒页图就能用。
  185. func startPagemapAsync() { go pagemapCached() }
  186. // pagemapH 分块返回页映射。
  187. //
  188. // /api/pagemap -> 第一块 (含 total/counts 等元信息)
  189. // /api/pagemap?meta=1 -> 只要元信息 (total/counts), 不返回页数据
  190. // /api/pagemap?start=1&count=20000 -> 指定区间
  191. //
  192. // 类型与使用率都编成 base64 字节串: 每页各 1 字节 (类型 +8 平移到无符号域),
  193. // 比 JSON 数字数组小一个量级。
  194. func pagemapH(w http.ResponseWriter, r *http.Request) {
  195. if dbF == "" {
  196. jw(w, map[string]any{"e": "no db"})
  197. return
  198. }
  199. q := r.URL.Query()
  200. idx := pagemapCached()
  201. meta := map[string]any{
  202. "total": idx.total, "counts": idx.counts,
  203. "allocated": idx.allocated, "neverUsed": idx.neverUsed,
  204. "firstFree": idx.firstFree, "chunk": pmChunkDefault,
  205. }
  206. if q.Get("meta") != "" {
  207. jw(w, meta)
  208. return
  209. }
  210. start, _ := strconv.Atoi(q.Get("start"))
  211. if start < 1 {
  212. start = 1
  213. }
  214. count, _ := strconv.Atoi(q.Get("count"))
  215. if count <= 0 || count > pmChunkMax {
  216. count = pmChunkDefault
  217. }
  218. if idx.total <= 0 {
  219. meta["start"], meta["count"], meta["done"] = 0, 0, true
  220. meta["t"], meta["u"] = "", ""
  221. jw(w, meta)
  222. return
  223. }
  224. if start > idx.total {
  225. start = idx.total + 1
  226. }
  227. end := start + count
  228. if end > idx.total+1 {
  229. end = idx.total + 1
  230. }
  231. tb := make([]byte, 0, end-start)
  232. ub := make([]byte, 0, end-start)
  233. for pid := start; pid < end; pid++ {
  234. tb = append(tb, byte(idx.typ[pid]+8))
  235. ub = append(ub, idx.usg[pid])
  236. }
  237. meta["start"], meta["count"], meta["done"] = start, end-start, end >= idx.total+1
  238. meta["t"] = base64.StdEncoding.EncodeToString(tb)
  239. meta["u"] = base64.StdEncoding.EncodeToString(ub)
  240. jw(w, meta)
  241. }