Merge branch 'feat/m31-reader-font-memo-and-read-badge'
This commit is contained in:
commit
3f09f7c137
12 files changed
+460
-53
No files matched your search
@@ -1,5 +1,6 @@
|
||||
# os.meow.reader 构建产物(不入仓)
|
||||
/vendor/
|
||||
# /vendor 不带尾斜杠:vendor 既可能是目录(真跑构建),也可能是链到主检出的软链(worktree 构建)。
|
||||
/vendor
|
||||
/reader
|
||||
/os.meow.reader.mpk
|
||||
/test/test_reader_core_host
|
||||
@@ -7,6 +8,8 @@
|
||||
/test/seg_smoke_host
|
||||
/test/chapter_smoke_host
|
||||
/test/shelf_group_smoke_host
|
||||
/test/font_resolve_smoke_host
|
||||
/test/font_resolve_bench_host
|
||||
/test/shelf_scan_bench_host
|
||||
/test/shelf_scan_bench_mipsel
|
||||
/test/rdft_smoke_mipsel
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"name":"Reader","exec":"/usr/data/mnt/sd_0/MeowAPPs/os.meow.reader/reader","package":"os.meow.reader","version":"0.1.3","version_code":4,"framework":1,"fonts":"common","author":"MeowOS","icon":"icon.png","volume_page":true}
|
||||
{"name":"Reader","exec":"/usr/data/mnt/sd_0/MeowAPPs/os.meow.reader/reader","package":"os.meow.reader","version":"0.1.4","version_code":5,"framework":1,"fonts":"common","author":"MeowOS","icon":"icon.png","volume_page":true}
|
||||
@@ -0,0 +1,76 @@
|
||||
/* app/font_resolve.c — 递归(深度 ≤ 3)扫 MeowOS/fonts 找首个 .otf/.ttf,进程内记忆化。
|
||||
*
|
||||
* 为什么缓存:fonts 树在 SD 卡(FUSE)上,每次开书都要调用;一次完整 walk 是
|
||||
* 「35+ 个目录的 opendir/readdir + 每个条目的 stat」,真机实测单次 10~250ms 抖动。
|
||||
*
|
||||
* 失效策略(红线:不许出现「明明有字库却说没有」):
|
||||
* 1) 只缓存成功路径。没找到 → 返回 NULL 且不写缓存,用户之后拷入字库,下次开书即可找到。
|
||||
* 2) 命中后仍做一次 access(路径, R_OK) 校验(1 次 stat):
|
||||
* 失败(被删/改名/不可读)→ 丢缓存重新 walk,立刻回到真实答案;
|
||||
* 成功 → 直接用。
|
||||
* 1 次 stat 比 35+ 个目录的 walk 便宜一到两个数量级(见 test/font_resolve_bench.c),
|
||||
* 所以正确性和省时两头都要。
|
||||
* 3) 命中与未命中的结果集与加缓存前一致:缓存前每次 walk 取的也是「readdir 顺序第一个」
|
||||
* 字库(同目录内容下顺序稳定),新增字库不会改变既有命中。
|
||||
* 4) 显式失效:font_cache_invalidate()(书架顶栏「刷新」调用)——用户新拷入字库且旧字库仍在时,
|
||||
* 缓存命中不会自动改选,刷新给一条确定的失效路径。
|
||||
*/
|
||||
#include "font_resolve.h"
|
||||
|
||||
#include <dirent.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define FONT_PATH_MAX 400
|
||||
#define FONT_SCAN_DEPTH 3
|
||||
|
||||
static char g_cached[FONT_PATH_MAX]; /* "" = 无有效缓存(从未找到,或已失效) */
|
||||
|
||||
/* 命中即写 out;0=没找到。 */
|
||||
static int find_font_rec(const char *dir, int depth, char *out, int outsz)
|
||||
{
|
||||
if (depth > FONT_SCAN_DEPTH) return 0;
|
||||
DIR *dp = opendir(dir);
|
||||
if (!dp) return 0;
|
||||
struct dirent *de;
|
||||
int found = 0;
|
||||
while ((de = readdir(dp)) && !found) {
|
||||
if (de->d_name[0] == '.') continue;
|
||||
char full[FONT_PATH_MAX + 80];
|
||||
if (snprintf(full, sizeof full, "%s/%s", dir, de->d_name) >= (int)sizeof full) continue;
|
||||
struct stat st;
|
||||
if (stat(full, &st) != 0) continue;
|
||||
if (S_ISDIR(st.st_mode)) { found = find_font_rec(full, depth + 1, out, outsz); }
|
||||
else {
|
||||
const char *d = strrchr(de->d_name, '.');
|
||||
if (d && (strcasecmp(d, ".otf") == 0 || strcasecmp(d, ".ttf") == 0)) {
|
||||
if (strlen(full) < (size_t)outsz) {
|
||||
strcpy(out, full);
|
||||
found = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
closedir(dp);
|
||||
return found;
|
||||
}
|
||||
|
||||
const char *font_resolve(const char *root)
|
||||
{
|
||||
if (g_cached[0]) {
|
||||
if (access(g_cached, R_OK) == 0) return g_cached; /* 命中:1 次 stat 校验 */
|
||||
g_cached[0] = 0; /* 字库没了:缓存失效,重扫 */
|
||||
}
|
||||
if (!root || !root[0]) return NULL;
|
||||
if (find_font_rec(root, 0, g_cached, (int)sizeof g_cached)) return g_cached;
|
||||
g_cached[0] = 0; /* 没找到:不缓存(之后拷入的字库下次仍能找到) */
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void font_cache_invalidate(void)
|
||||
{
|
||||
g_cached[0] = 0;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/* app/font_resolve.h — 在 MeowOS/fonts 里找字体文件(进程内记忆化)。 */
|
||||
#ifndef FONT_RESOLVE_H
|
||||
#define FONT_RESOLVE_H
|
||||
|
||||
/* 递归(深度 ≤ 3)扫 root,返回第一个 .otf/.ttf 的完整路径;没找到返回 NULL。
|
||||
* 返回值指向模块内部静态缓冲,调用方不得 free,下次调用前一直有效。
|
||||
* 语义:只缓存成功路径;命中时做一次 access() 校验,字库被删即重新扫描;
|
||||
* 失败不缓存,用户之后拷入字库仍能立刻找到。 */
|
||||
const char *font_resolve(const char *root);
|
||||
|
||||
/* 丢弃缓存(下次 font_resolve 强制重扫)。书架顶栏「刷新」时调用:
|
||||
* 命中路径仍在时不会自动发现「用户新拷入、路序更靠前」的字库,手动刷新给一条显式失效路径。 */
|
||||
void font_cache_invalidate(void);
|
||||
|
||||
#endif /* FONT_RESOLVE_H */
|
||||
@@ -8,10 +8,10 @@
|
||||
#include "../reader_internal.h"
|
||||
#include "meow_ui_internal.h" /* back_buf/pitch/bpp 访问器(决策见报告) */
|
||||
#include "book.h"
|
||||
#include "font_resolve.h"
|
||||
#include "progress.h"
|
||||
#include "reader_conf.h"
|
||||
#include "rdchapter.h" /* RDCHAP_TITLE_MAX(章节标题) */
|
||||
#include <dirent.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -45,7 +45,6 @@ static int g_jump_scroll; /* 列表滚动(首行项下标) */
|
||||
static char g_jump_buf[8]; /* 章内/段内页码输入缓冲(ASCII 数字) */
|
||||
static int g_chap_cache_idx = -1; /* 章节范围缓存(章起/止页;随字号/行距重排失效) */
|
||||
static int g_chap_cache_start, g_chap_cache_end;
|
||||
static char g_font_path[400];
|
||||
static int g_notice_ms;
|
||||
|
||||
/* ---- R3 翻页动画(轻量水平滑入) ---- */
|
||||
@@ -141,39 +140,10 @@ static void reader_set_bg(const char *bg)
|
||||
reader_redraw();
|
||||
}
|
||||
|
||||
/* 递归(深度≤3)扫 MeowOS/fonts/ 找第一个 .otf/.ttf。0=找到。 */
|
||||
static int find_font_rec(const char *dir, int depth)
|
||||
{
|
||||
if (depth > 3) return 0;
|
||||
DIR *dp = opendir(dir);
|
||||
if (!dp) return 0;
|
||||
struct dirent *de;
|
||||
int found = 0;
|
||||
while ((de = readdir(dp)) && !found) {
|
||||
if (de->d_name[0] == '.') continue;
|
||||
char full[480];
|
||||
if (snprintf(full, sizeof full, "%s/%s", dir, de->d_name) >= (int)sizeof full) continue;
|
||||
struct stat st;
|
||||
if (stat(full, &st) != 0) continue;
|
||||
if (S_ISDIR(st.st_mode)) { found = find_font_rec(full, depth + 1); }
|
||||
else {
|
||||
const char *d = strrchr(de->d_name, '.');
|
||||
if (d && (strcasecmp(d, ".otf") == 0 || strcasecmp(d, ".ttf") == 0)) {
|
||||
if (strlen(full) < sizeof g_font_path) {
|
||||
strcpy(g_font_path, full);
|
||||
found = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
closedir(dp);
|
||||
return found;
|
||||
}
|
||||
/* MeowOS/fonts/ 找第一个 .otf/.ttf(M31:路径进程内记忆化,见 font_resolve.c)。 */
|
||||
static const char *resolve_font(void)
|
||||
{
|
||||
g_font_path[0] = 0;
|
||||
if (find_font_rec(READER_SD_ROOT "/MeowOS/fonts", 0)) return g_font_path;
|
||||
return NULL;
|
||||
return font_resolve(READER_SD_ROOT "/MeowOS/fonts");
|
||||
}
|
||||
|
||||
static void chapter_name(void)
|
||||
|
||||
@@ -20,8 +20,11 @@
|
||||
* 行为变化:新放进 Books/ 的书需点一次「刷新」才出现。
|
||||
* - **行数多时关圆角容器**(见 SHELF_ROUND_MAX_ROWS):圆角卡逐像素填充,595 本时滑一步 46ms。
|
||||
* M30:M29 的「刚读完不置顶」缺口补上(只刷一本书的 ts + 内存重排,不重扫全盘)。
|
||||
* M31:置顶时一并重算这本的副题(shelf_fmt_sub),「· 读」立即出现,不等下一次全量刷新;
|
||||
* 顶栏「刷新」同时丢弃字体路径缓存(新拷入的字库此时生效)。
|
||||
*/
|
||||
#include "../reader_internal.h"
|
||||
#include "font_resolve.h"
|
||||
#include "progress.h"
|
||||
#include "reader_conf.h"
|
||||
#include "shelf_model.h"
|
||||
@@ -115,7 +118,9 @@ static void build_rows(void)
|
||||
}
|
||||
|
||||
/* M30:从阅读页返回时把「刚读完」置顶。只读这一本的进度文件 + 重排内存里的 g_books,
|
||||
* 不 opendir/不遍历目录(对比全量重扫真机 3s)。重排后 on_enter 照常 build_rows + 重建控件。 */
|
||||
* 不 opendir/不遍历目录(对比全量重扫真机 3s)。重排后 on_enter 照常 build_rows + 重建控件。
|
||||
* M31:置顶的同时重建这本的副题——只改 ts 不重建的话,副题的「· 读」要等下次全量刷新
|
||||
* (scan_books 重新格式化)才出现,表现就是「刚读完却没显示已读」。 */
|
||||
static void promote_just_read(void)
|
||||
{
|
||||
if (!g_just_read[0]) return;
|
||||
@@ -124,6 +129,7 @@ static void promote_just_read(void)
|
||||
uint64_t ts = shelf_ts_of(g_books[i].path);
|
||||
if (ts > g_books[i].ts) {
|
||||
g_books[i].ts = ts;
|
||||
shelf_fmt_sub(&g_books[i]); /* 副题随 ts 重算,「· 读」立即出现 */
|
||||
shelf_sort_recent(g_books, g_nbooks);
|
||||
}
|
||||
break;
|
||||
@@ -433,6 +439,7 @@ int shelf_on_tap(int x, int y)
|
||||
return 1;
|
||||
} else if (bi == 2) { /* 刷新:强制重扫(缓存失效 + 重扫;新书/刚读完的书在此出现) */
|
||||
g_scanned = 0;
|
||||
font_cache_invalidate(); /* 字体路径记忆化也一并失效(新拷入的字库在此生效) */
|
||||
scan_books(); build_rows();
|
||||
shelf_build_content();
|
||||
shelf_redraw();
|
||||
|
||||
@@ -69,6 +69,30 @@ void shelf_sort_recent(shelf_book *books, int nbooks)
|
||||
qsort(books, (size_t)nbooks, sizeof(shelf_book), cmp_recent_then_name);
|
||||
}
|
||||
|
||||
/* 重建一本书的两种副题(扫描后与「刚读完置顶」共用同一份格式化逻辑)。
|
||||
* sub = 格式 · 大小 [· 读](format 模式/宫格)
|
||||
* sub_folder = 相对 book.group 第一级之后的子路径 · sub(folder 模式)
|
||||
*/
|
||||
void shelf_fmt_sub(shelf_book *b)
|
||||
{
|
||||
if (!b) return;
|
||||
char sz[24];
|
||||
shelf_fmt_size(sz, sizeof sz, b->size);
|
||||
/* 默认副题:与 M27 前逐字一致(format 模式/宫格都用它)。 */
|
||||
snprintf(b->sub, sizeof b->sub, "%s · %s%s", shelf_fmt_name(b->fmt), sz,
|
||||
b->ts ? " · 读" : "");
|
||||
/* folder 副题:第一级之后的子路径(分类/作品)前置,其余同默认副题。 */
|
||||
const char *rest = strchr(b->group, '/');
|
||||
if (rest) rest++;
|
||||
if (rest && rest[0]) {
|
||||
char pre[100];
|
||||
shelf_copy_utf8(pre, (int)sizeof pre, rest);
|
||||
snprintf(b->sub_folder, sizeof b->sub_folder, "%s · %s", pre, b->sub);
|
||||
} else {
|
||||
snprintf(b->sub_folder, sizeof b->sub_folder, "%s", b->sub);
|
||||
}
|
||||
}
|
||||
|
||||
typedef struct scan_ctx {
|
||||
shelf_book *books;
|
||||
int cap;
|
||||
@@ -146,21 +170,7 @@ static void scan_dir(scan_ctx *c, const char *dir, int depth, const char *rel, c
|
||||
b->fmt = f;
|
||||
b->size = (long)st.st_size;
|
||||
b->ts = c->ts_fn ? c->ts_fn(b->path) : 0;
|
||||
char sz[24];
|
||||
shelf_fmt_size(sz, sizeof sz, b->size);
|
||||
/* 默认副题:与 M27 前逐字一致(format 模式/宫格都用它)。 */
|
||||
snprintf(b->sub, sizeof b->sub, "%s · %s%s", shelf_fmt_name(f), sz,
|
||||
b->ts ? " · 读" : "");
|
||||
/* folder 副题:第一级之后的子路径(分类/作品)前置,其余同默认副题。 */
|
||||
const char *rest = strchr(rel, '/');
|
||||
if (rest) rest++;
|
||||
if (rest && rest[0]) {
|
||||
char pre[100];
|
||||
shelf_copy_utf8(pre, (int)sizeof pre, rest);
|
||||
snprintf(b->sub_folder, sizeof b->sub_folder, "%s · %s", pre, b->sub);
|
||||
} else {
|
||||
snprintf(b->sub_folder, sizeof b->sub_folder, "%s", b->sub);
|
||||
}
|
||||
shelf_fmt_sub(b); /* 副题:格式 · 大小 [· 读](+ folder 子路径) */
|
||||
c->n++;
|
||||
}
|
||||
closedir(dp);
|
||||
|
||||
@@ -79,6 +79,8 @@ int shelf_build_rows(const shelf_book *books, int nbooks, int folder_mode,
|
||||
int shelf_fmt_from_ext(const char *name);
|
||||
const char *shelf_fmt_name(int fmt);
|
||||
void shelf_fmt_size(char *out, int outsz, long size);
|
||||
/* 按 fmt/size/ts/group 重建 b->sub 与 b->sub_folder(扫描后与「刚读完置顶」共用)。 */
|
||||
void shelf_fmt_sub(shelf_book *b);
|
||||
/* UTF-8 安全截断:最多写 cap-1 字节;若截断点落在多字节序列中间,整段丢弃该残缺字符。
|
||||
* 返回写入字节数(不含 NUL)。 */
|
||||
int shelf_copy_utf8(char *dst, int cap, const char *src);
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
# 产物:
|
||||
# reader mipsel 静态 ELF(链 meow_ui.a + freetype + miniz + libmobi + zlib)
|
||||
# test/test_reader_core_host 宿主 core 单测(enc/layout/txt)
|
||||
# test/shelf_group_smoke_host 宿主书架分组单测(M27/M31 副题)
|
||||
# test/font_resolve_smoke_host 宿主字体路径记忆化单测(M31)
|
||||
# test/font_resolve_bench_host 宿主字体 walk/命中耗时对比(M31,只编不跑)
|
||||
# test/loaders_smoke_host 宿主四格式 loader 单测
|
||||
# test/rdft_smoke_mipsel qemu-mipsel FreeType 冒烟
|
||||
# test/book_smoke_mipsel qemu-mipsel TXT 书桥冒烟
|
||||
@@ -116,6 +119,11 @@ echo "==> host shelf group test (M27)"
|
||||
$HOST_CC -O2 -Wall -Wextra -Iapp -I../sdk test/shelf_group_smoke.c app/shelf_model.c app/progress.c app/reader_conf.c -o test/shelf_group_smoke_host
|
||||
./test/shelf_group_smoke_host
|
||||
|
||||
echo "==> host font resolve test (M31)"
|
||||
$HOST_CC -O2 -Wall -Wextra -Iapp test/font_resolve_smoke.c app/font_resolve.c -o test/font_resolve_smoke_host
|
||||
./test/font_resolve_smoke_host
|
||||
$HOST_CC -O2 -Wall -Wextra -Iapp test/font_resolve_bench.c app/font_resolve.c -o test/font_resolve_bench_host
|
||||
|
||||
echo "==> host shelf scan bench (M27,只编不跑;用法 ./test/shelf_scan_bench_host <Books目录> [progress|noop] [轮数])"
|
||||
$HOST_CC -O2 -Wall -Wextra -Iapp -I../sdk test/shelf_scan_bench.c app/shelf_model.c app/progress.c -o test/shelf_scan_bench_host
|
||||
|
||||
@@ -161,7 +169,7 @@ run_qemu 150 "$QEMU_BIN" ./test/formats_smoke_mipsel "${FONT:-$ROOT/font/src/MiS
|
||||
echo "==> reader binary"
|
||||
CORE_L="core/rdenc.c core/rdlayout.c core/rdtxt.c core/seg.c core/rdchapter.c core/rdxml.c core/rdhtml.c core/rdsb.c core/rdzip.c core/rdsrc.c core/rdepub.c core/rdfb2.c core/rdmobi.c core/rdcbz.c"
|
||||
ENGINE_L="engine/rdft.c"
|
||||
APP_L="app.c app/shelf.c app/shelf_model.c app/reader.c app/book.c app/progress.c app/settings.c app/reader_conf.c app/reader_ipc.c"
|
||||
APP_L="app.c app/shelf.c app/shelf_model.c app/font_resolve.c app/reader.c app/book.c app/progress.c app/settings.c app/reader_conf.c app/reader_ipc.c"
|
||||
$CC -Os -static -Wall -Wextra -I../sdk -I../meow_launcherd -I. -Icore -Iapp -Iengine $FT_INCS $COREINC \
|
||||
$CORE_L $APP_L $ENGINE_L "$OBJ"/mz_*.o "$OBJ"/lm_*.o "$OBJ"/rdimg.o \
|
||||
../sdk/meow_ui.a "$SYSROOT/lib/libz.a" "$FT_BUILD/libfreetype.a" -lpthread -o reader
|
||||
@@ -173,7 +181,7 @@ echo " 运行(qemu): qemu-mipsel ./reader"
|
||||
# ---------- 8) 真机耗时基准(不进发行包:与 reader 同源,只把 app.c 的 main 换成 bench main) ----------
|
||||
# 用法(设备):MEOW_FONT_* 等 env 与 App 一致时执行 ./shelf_bench_ondev_mipsel [书...]
|
||||
echo "==> ondev shelf bench (not shipped)"
|
||||
APP_BENCH_L="app/shelf.c app/shelf_model.c app/reader.c app/book.c app/progress.c app/settings.c app/reader_conf.c app/reader_ipc.c"
|
||||
APP_BENCH_L="app/shelf.c app/shelf_model.c app/font_resolve.c app/reader.c app/book.c app/progress.c app/settings.c app/reader_conf.c app/reader_ipc.c"
|
||||
$CC -Os -static -Wall -Wextra -I../sdk -I../meow_launcherd -I. -Icore -Iapp -Iengine $FT_INCS $COREINC \
|
||||
test/shelf_bench_ondev.c $CORE_L $APP_BENCH_L $ENGINE_L "$OBJ"/mz_*.o "$OBJ"/lm_*.o "$OBJ"/rdimg.o \
|
||||
../sdk/meow_ui.a "$SYSROOT/lib/libz.a" "$FT_BUILD/libfreetype.a" -lpthread -o test/shelf_bench_ondev_mipsel
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
/* test/font_resolve_bench.c — M31 字体路径记忆化耗时对比(宿主,不依赖设备)。
|
||||
*
|
||||
* 同一棵树两种口径:
|
||||
* walk = 每次都重扫(= 加缓存前的 resolve_font 行为),由 font_cache_invalidate() 强制;
|
||||
* hit = 缓存命中 + access() 校验(= 加缓存后开书路径)。
|
||||
* 用法:./font_resolve_bench_host [目录] [迭代数],不传目录则先造一棵 35 目录的树。
|
||||
*/
|
||||
#include "font_resolve.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static void mkdirs(const char *path)
|
||||
{
|
||||
char t[512];
|
||||
snprintf(t, sizeof t, "%s", path);
|
||||
for (char *p = t + 1; *p; p++) {
|
||||
if (*p == '/') { *p = 0; mkdir(t, 0755); *p = '/'; }
|
||||
}
|
||||
mkdir(t, 0755);
|
||||
}
|
||||
|
||||
static void write_file(const char *path, const char *body)
|
||||
{
|
||||
FILE *f = fopen(path, "wb");
|
||||
if (!f) return;
|
||||
fputs(body, f);
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
static void make_fonts_tree(const char *root, int ndirs, int files_per_dir)
|
||||
{
|
||||
char p[512];
|
||||
for (int i = 0; i < ndirs; i++) {
|
||||
snprintf(p, sizeof p, "%s/family_%02d/sub", root, i);
|
||||
mkdirs(p);
|
||||
for (int j = 0; j < files_per_dir; j++) {
|
||||
snprintf(p, sizeof p, "%s/family_%02d/readme_%d.txt", root, i, j);
|
||||
write_file(p, "x");
|
||||
}
|
||||
snprintf(p, sizeof p, "%s/family_%02d/sub/Font%02d.ttf", root, i, i);
|
||||
write_file(p, "x");
|
||||
}
|
||||
}
|
||||
|
||||
static double ms_now(void)
|
||||
{
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (double)ts.tv_sec * 1000.0 + (double)ts.tv_nsec / 1e6;
|
||||
}
|
||||
|
||||
typedef struct stat_res { double min, avg, max; } stat_res;
|
||||
|
||||
static stat_res run(const char *root, int iters, int invalidate_each)
|
||||
{
|
||||
stat_res r = { 1e9, 0, 0 };
|
||||
double sum = 0;
|
||||
for (int i = 0; i < iters; i++) {
|
||||
if (invalidate_each) font_cache_invalidate();
|
||||
double t0 = ms_now();
|
||||
(void)font_resolve(root);
|
||||
double dt = ms_now() - t0;
|
||||
if (dt < r.min) r.min = dt;
|
||||
if (dt > r.max) r.max = dt;
|
||||
sum += dt;
|
||||
}
|
||||
r.avg = iters ? sum / (double)iters : 0;
|
||||
return r;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
const char *root = (argc > 1) ? argv[1] : NULL;
|
||||
int iters = (argc > 2) ? atoi(argv[2]) : 200;
|
||||
char tmpbuf[512] = { 0 };
|
||||
|
||||
if (!root) {
|
||||
const char *base = getenv("TMPDIR");
|
||||
if (!base || !base[0]) base = "/tmp";
|
||||
snprintf(tmpbuf, sizeof tmpbuf, "%s/fontbench_XXXXXX", base);
|
||||
if (!mkdtemp(tmpbuf)) { printf("mkdtemp 失败\n"); return 2; }
|
||||
make_fonts_tree(tmpbuf, 35, 3);
|
||||
root = tmpbuf;
|
||||
}
|
||||
/* 预热(首轮含 dentry 冷缓存,不计入) */
|
||||
font_cache_invalidate(); font_resolve(root);
|
||||
font_resolve(root);
|
||||
|
||||
const char *p = font_resolve(root);
|
||||
printf("root=%s\n", root);
|
||||
printf("font=%s\n", p ? p : "(none)");
|
||||
stat_res w = run(root, iters, 1); /* 每次失效 = 旧行为:每次开书重 walk */
|
||||
stat_res h = run(root, iters, 0); /* 缓存命中 + access 校验 = 新行为 */
|
||||
printf("walk (每次重扫): min=%.3f avg=%.3f max=%.3f ms\n", w.min, w.avg, w.max);
|
||||
printf("hit (缓存+access): min=%.3f avg=%.3f max=%.3f ms\n", h.min, h.avg, h.max);
|
||||
if (h.avg > 0) printf("提速: %.1fx\n", w.avg / h.avg);
|
||||
|
||||
if (tmpbuf[0]) {
|
||||
char cmd[600];
|
||||
snprintf(cmd, sizeof cmd, "rm -rf '%s'", tmpbuf);
|
||||
if (system(cmd) != 0) { /* 忽略 */ }
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
/* test/font_resolve_smoke.c — M31 字体路径记忆化宿主单测(纯 POSIX,不依赖设备/SD 卡)。
|
||||
*
|
||||
* 覆盖缓存的三条红线:
|
||||
* F1 没找到 → NULL,且**不缓存失败**:之后拷入字库,下一次调用就能找到;
|
||||
* F2 命中 → 后续调用返回同一路径;
|
||||
* F3 命中后字库被删/改名 → access() 校验失败 → 重扫,立刻回到「没有字库」的真实答案;
|
||||
* 重新放回后又能找到(缓存不会一直错下去)。
|
||||
* 另测与加缓存前一致的扫描语义(深度上限、首个字库、忽略隐藏文件/非字体扩展名)。
|
||||
*
|
||||
* 用法:./font_resolve_smoke_host [临时目录],默认用 TMPDIR。
|
||||
*/
|
||||
#include "font_resolve.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static int g_pass = 0, g_fail = 0;
|
||||
#define CHECK(cond, msg) do { \
|
||||
if (cond) g_pass++; \
|
||||
else { g_fail++; printf("FAIL %s:%d %s\n", __FILE__, __LINE__, msg); } \
|
||||
} while (0)
|
||||
|
||||
static void mkdirs(const char *path)
|
||||
{
|
||||
char t[512];
|
||||
snprintf(t, sizeof t, "%s", path);
|
||||
for (char *p = t + 1; *p; p++) {
|
||||
if (*p == '/') { *p = 0; mkdir(t, 0755); *p = '/'; }
|
||||
}
|
||||
mkdir(t, 0755);
|
||||
}
|
||||
|
||||
static void write_file(const char *path, const char *body)
|
||||
{
|
||||
FILE *f = fopen(path, "wb");
|
||||
if (!f) { printf("无法创建 %s\n", path); exit(2); }
|
||||
fputs(body, f);
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
/* 真机形态:MeowOS/fonts 下 35 个目录,每个目录若干非字体文件 + 1 个字体。
|
||||
* 字体放在子目录里,让「首个命中」落在 walk 较深处(接近 survey 的真机耗时场景)。 */
|
||||
static void make_fonts_tree(const char *root, int ndirs)
|
||||
{
|
||||
char p[512];
|
||||
for (int i = 0; i < ndirs; i++) {
|
||||
snprintf(p, sizeof p, "%s/family_%02d/sub", root, i);
|
||||
mkdirs(p);
|
||||
for (int j = 0; j < 3; j++) {
|
||||
snprintf(p, sizeof p, "%s/family_%02d/readme_%d.txt", root, i, j);
|
||||
write_file(p, "x");
|
||||
}
|
||||
snprintf(p, sizeof p, "%s/family_%02d/sub/Font%02d.ttf", root, i, i);
|
||||
write_file(p, "x");
|
||||
}
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
const char *base = getenv("TMPDIR");
|
||||
if (!base || !base[0]) base = "/tmp";
|
||||
char tmpl[512];
|
||||
snprintf(tmpl, sizeof tmpl, "%s/font_resolve_XXXXXX", base);
|
||||
char *tmp = mkdtemp(tmpl);
|
||||
if (!tmp) { printf("mkdtemp 失败\n"); return 2; }
|
||||
|
||||
char root[512];
|
||||
snprintf(root, sizeof root, "%s/fonts", tmp);
|
||||
mkdirs(root);
|
||||
|
||||
/* F1 空目录:没找到,且失败不缓存 */
|
||||
CHECK(font_resolve(root) == NULL, "空目录 → NULL");
|
||||
CHECK(font_resolve(root) == NULL, "紧接着再调仍 NULL(失败未缓存成「找到」)");
|
||||
|
||||
/* F1b 空目录之后再拷入字库:下一次调用就能找到(「用户后来放了字库」) */
|
||||
char f1[512];
|
||||
snprintf(f1, sizeof f1, "%s/late.ttf", root);
|
||||
write_file(f1, "x");
|
||||
const char *p = font_resolve(root);
|
||||
CHECK(p && strcmp(p, f1) == 0, "失败后拷入字库 → 下次调用即找到");
|
||||
|
||||
/* F2 命中:连续调用返回同一路径(缓存生效,不重新 walk) */
|
||||
const char *q = font_resolve(root);
|
||||
CHECK(q && strcmp(q, f1) == 0, "命中后返回同一路径");
|
||||
/* 命中期间新增字库不改选(与「取 readdir 顺序第一个」的既有语义一致;
|
||||
* 显式换字体走顶栏「刷新」→ font_cache_invalidate) */
|
||||
char f1b[512];
|
||||
snprintf(f1b, sizeof f1b, "%s/aaa.otf", root);
|
||||
write_file(f1b, "x");
|
||||
CHECK(font_resolve(root) && strcmp(font_resolve(root), f1) == 0,
|
||||
"命中期间新增字库不改选(保持一致,刷新可强制重选)");
|
||||
|
||||
/* F3 字库被删:access() 校验失败 → 重扫 → NULL(不许「明明没有却说有」,也不许反过来卡死) */
|
||||
unlink(f1);
|
||||
unlink(f1b);
|
||||
CHECK(font_resolve(root) == NULL, "缓存路径被删 → 下一次调用即重扫得到 NULL");
|
||||
|
||||
/* F3b 放回不同名的字库:仍能找到(缓存失效状态不会卡住) */
|
||||
char f3[512];
|
||||
snprintf(f3, sizeof f3, "%s/back.ttf", root);
|
||||
write_file(f3, "x");
|
||||
p = font_resolve(root);
|
||||
CHECK(p && strcmp(p, f3) == 0, "删掉后放回字库 → 重新找到");
|
||||
|
||||
/* F4 多目录树:首个命中 = walk 顺序第一个(35 个目录时的行为与缓存前一致) */
|
||||
char big[512];
|
||||
snprintf(big, sizeof big, "%s/bigfonts", tmp);
|
||||
make_fonts_tree(big, 35);
|
||||
font_cache_invalidate(); /* 换树前清缓存(等价于顶栏刷新) */
|
||||
const char *b = font_resolve(big);
|
||||
CHECK(b && strstr(b, "/family_") != NULL && strcmp(b + strlen(b) - 4, ".ttf") == 0,
|
||||
"35 目录树 → 返回字体文件路径");
|
||||
const char *b2 = font_resolve(big);
|
||||
CHECK(b2 && strcmp(b2, b) == 0, "大树上命中后路径稳定");
|
||||
|
||||
/* F5 深度上限(相对 root:目录深度 > 3 的字库不收,与缓存前一致) */
|
||||
char dfroot[512];
|
||||
snprintf(dfroot, sizeof dfroot, "%s/deepfonts", tmp);
|
||||
char deep[512];
|
||||
snprintf(deep, sizeof deep, "%s/a/b/c/d", dfroot);
|
||||
mkdirs(deep);
|
||||
char dp[512];
|
||||
snprintf(dp, sizeof dp, "%s/too_deep.ttf", deep); /* root/a/b/c/d/ → 深度 4 */
|
||||
write_file(dp, "x");
|
||||
font_cache_invalidate();
|
||||
CHECK(font_resolve(dfroot) == NULL, "深度 > 3 的字库不收(与缓存前扫描口径一致)");
|
||||
/* 第 3 层仍收 */
|
||||
char ok3[512];
|
||||
snprintf(ok3, sizeof ok3, "%s/a/b/c/ok3.ttf", dfroot); /* root/a/b/c/ → 深度 3 */
|
||||
write_file(ok3, "x");
|
||||
font_cache_invalidate();
|
||||
p = font_resolve(dfroot);
|
||||
CHECK(p && strcmp(p, ok3) == 0, "第 3 层目录的字库收(深度上限边界)");
|
||||
|
||||
/* F6 忽略隐藏文件/非字体扩展名 */
|
||||
char hid[512];
|
||||
snprintf(hid, sizeof hid, "%s/hidden", tmp);
|
||||
mkdirs(hid);
|
||||
char hp[512];
|
||||
snprintf(hp, sizeof hp, "%s/.hidden.ttf", hid);
|
||||
write_file(hp, "x");
|
||||
snprintf(hp, sizeof hp, "%s/notafont.txt", hid);
|
||||
write_file(hp, "x");
|
||||
font_cache_invalidate();
|
||||
CHECK(font_resolve(hid) == NULL, "隐藏文件/非字体扩展名不被当作字库");
|
||||
|
||||
/* F7 root 非法 */
|
||||
font_cache_invalidate();
|
||||
CHECK(font_resolve(NULL) == NULL && font_resolve("") == NULL, "root=NULL/空 → NULL");
|
||||
char missing[512];
|
||||
snprintf(missing, sizeof missing, "%s/no_such_dir", tmp);
|
||||
CHECK(font_resolve(missing) == NULL, "root 不存在 → NULL");
|
||||
|
||||
printf("font_resolve_smoke: %d pass, %d fail\n", g_pass, g_fail);
|
||||
char cmd[600];
|
||||
snprintf(cmd, sizeof cmd, "rm -rf '%s'", tmp);
|
||||
if (system(cmd) != 0) { /* 忽略 */ }
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -11,6 +11,8 @@
|
||||
* G7 超上限不静默:truncated=1(含「cap 卡在目录边界」与「恰好装满不误报」两种);
|
||||
* 行数上限(段头 ≤ 书数 ⇒ 2N 必够)
|
||||
* G8 进度不受分组影响:path 不变、rdprog_load 前后同值、ts 两种模式一致
|
||||
* G9 副题格式化的唯一实现 shelf_fmt_sub:扫描与「刚读完置顶」共用
|
||||
* (ts 变化后重算 → 「· 读」立即出现;folder 子路径前缀随 group 重建)
|
||||
*/
|
||||
#include "shelf_model.h"
|
||||
#include "progress.h"
|
||||
@@ -297,6 +299,48 @@ static void test_scan_and_modes(const char *root, const char *tmproot)
|
||||
CHECK(shelf_copy_utf8(dst, 96, ascii) == 95, "ASCII 90+ 字节按 cap-1 截断");
|
||||
}
|
||||
|
||||
/* G9(M31):副题格式化的唯一实现 —— 扫描与「刚读完置顶」共用 shelf_fmt_sub。
|
||||
* 置顶只改 ts 不重算副题时,「· 读」要等一次全量刷新才出现(M30 遗留),这里锁住语义。 */
|
||||
static void test_fmt_sub(void)
|
||||
{
|
||||
shelf_book b;
|
||||
memset(&b, 0, sizeof b);
|
||||
strcpy(b.title, "t");
|
||||
b.fmt = 0; b.size = 5;
|
||||
shelf_fmt_sub(&b);
|
||||
CHECK(strcmp(b.sub, "TXT · 5B") == 0, "无进度副题不含「读」");
|
||||
CHECK(strcmp(b.sub_folder, b.sub) == 0, "根下书两种副题相同");
|
||||
|
||||
/* 刚读完:ts 从 0 变正 → 重算后「· 读」立即出现(不等全量刷新) */
|
||||
b.ts = 100;
|
||||
shelf_fmt_sub(&b);
|
||||
CHECK(strcmp(b.sub, "TXT · 5B · 读") == 0, "ts>0 重算后副题含「· 读」");
|
||||
CHECK(strcmp(b.sub_folder, b.sub) == 0, "根下书重算后两种副题仍相同");
|
||||
|
||||
/* folder 模式:子路径前置,其余与默认副题一致 */
|
||||
shelf_book f;
|
||||
memset(&f, 0, sizeof f);
|
||||
strcpy(f.title, "f");
|
||||
strcpy(f.group, "a/b");
|
||||
f.fmt = 2; f.size = 4 * 1024;
|
||||
shelf_fmt_sub(&f);
|
||||
CHECK(strcmp(f.sub, "EPUB · 4KB") == 0, "folder 书的默认副题不带子路径");
|
||||
CHECK(strcmp(f.sub_folder, "b · EPUB · 4KB") == 0, "folder 副题 = 子路径 · 默认副题");
|
||||
f.ts = 7;
|
||||
shelf_fmt_sub(&f);
|
||||
CHECK(strcmp(f.sub_folder, "b · EPUB · 4KB · 读") == 0,
|
||||
"folder 副题重算后「· 读」出现在末尾");
|
||||
|
||||
/* 一级目录书:rel 无 '/' → 两种副题相同 */
|
||||
shelf_book g;
|
||||
memset(&g, 0, sizeof g);
|
||||
strcpy(g.group, "轻小说");
|
||||
g.fmt = 3; g.size = 100;
|
||||
shelf_fmt_sub(&g);
|
||||
CHECK(strcmp(g.sub, "MOBI · 100B") == 0 && strcmp(g.sub_folder, g.sub) == 0,
|
||||
"一级目录书无子路径前缀");
|
||||
}
|
||||
|
||||
static void test_progress_stability(const char *root)
|
||||
{
|
||||
/* G8:写一份进度,切换分组模式前后 rdprog_load 必须同值,path 不变。 */
|
||||
@@ -382,6 +426,7 @@ int main(void)
|
||||
mkdirs(root);
|
||||
|
||||
test_scan_and_modes(root, tmp);
|
||||
test_fmt_sub();
|
||||
test_progress_stability(root);
|
||||
test_conf_default(tmp);
|
||||
|
||||
|
||||
Reference in new issue
Block a user