RenderCacheFolder در HotPDF کش صفحات رندرشدهٔ درونحافظهای کامپوننت HotPDF در Delphi را به یک کش صفحهٔ ماندگار روی دیسک تبدیل میکند: صفحات رندرشده بهشکل فایلهای PNG زیر پوشهای که انتخاب میکنی نوشته میشوند و دفعهٔ بعد که همان منبع PDF باز شود، RenderLoadedPageToBitmapCached آنها را میخواند بهجای rasterize دوباره. ترتیب lookup حافظه است، بعد دیسک، بعد renderer
لایهٔ دیسک از v2.416.0 در API بوده، اما تا v2.770.140 هرگز برای یک فراخوانی عادی LoadFromFile یا LoadFromStream واقعاً صفحهای سرو نمیکرد. فیکس سؤالی را پیش کشید که هر cache ماندگاری باید جوابش را بدهد: از کجا میدانی فایلی که امروز باز کردهای همان سندی است که دیروز رندر کردی، و با صفحات cacheشده وقتی نیست چه میشود؟ پایین جوابهایی است که HotPDF رویشان ایستاد، از جمله جاهایی که عمداً از cache کردن سر باز میزند
کش رندر روی دیسک در HotPDF چطور کار میکند؟
کش رندر روی دیسک در HotPDF لایهٔ دومی پشت کش raster درونحافظهای است و فقط وقتی وارد میشود که RenderCacheFolder یک مسیر غیرخالی باشد. یک فراخوانی RenderLoadedPageToBitmapCached(PageIndex, DPI) اول مدخلهای درونحافظهای را اسکن میکند، با کلید اندیس صفحه و DPI و یک واریانت تنظیمات رندر. در صورت miss از لایهٔ دیسک میپرسد؛ یک hit دیسکی PNG را decode میکند، به حافظه برمیگرداندش و یک کپی متعلق به فراخواننده تحویل میدهد. فقط وقتی هر دو لایه miss کنند صفحه از مفسر content-stream عبور میکند که در رندر کردن یک صفحهٔ PDF بارگذاریشده به TBitmap توضیح داده شد، و بیتمپ تازه بعدش روی دیسک هم نوشته میشود
روی دیسک چیدمان عمداً خستهکننده است. هر سند یک زیرپوشه میگیرد با نامی از یک کلید سند 16 نویسهٔ هگز بهعلاوهٔ یک واریانت رندر 16 نویسهٔ هگز، هر صفحه بهشکل <page>@<dpi>.png ذخیره میشود، و یک index.txt در ریشه اسناد را پشت یک تگ schema بهترتیب اخیراً-استفادهشده نگه میدارد. ناهمخوانی schema در اولین استفاده پوشه را پاک میکند. نوشتنها اول به یک فایل موقت میروند و با یک جایگزینی اتمی سر جایشان سواپ میشوند، پس یک crash وسط نوشتن یا صفحهٔ قدیمی را میگذارد یا هیچ، هرگز نصف یک PNG نه. یک PNG که decode نشود حذف و بهعنوان miss شمرده میشود
سه محدودیت پوشه را کپ میکنند:
RenderCacheMaxDocuments(پیشفرض 20) تعداد زیرپوشههای سند را کپ میکند؛ کمتر اخیراً-استفادهشده اول اخراج میشودRenderCacheMaxBytes(پیشفرض 524288000 که همان 500 مگابایت است) اندازهٔ کل فایلهای PNG زیر ریشه را کپ میکند- هر پوشهٔ سند حداکثر 200 تصویر صفحه نگه میدارد؛ آن سقف بهازای هر سند توسط THotPDF ثابت شده و یک property منتشرشده نیست
RenderCacheCapacity (پیشفرض 8) یک پیچ جداگانه است: تنظیم میکند لایهٔ درونحافظهای چند صفحهٔ رندرشده را نگه دارد و هیچ ربطی به جایپای دیسکی ندارد
uses
SysUtils, Graphics, HPDFDoc;
procedure WarmThumbnails(const FileName: string);
var
Pdf: THotPDF;
Bmp: TBitmap;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
// لایهٔ دیسک را قبل از اولین رندر cacheشده تنظیم کن:
// پوشه و هر دو محدودیت وقتی لایه اولین بار استفاده میشود خوانده میشوند
Pdf.RenderCacheFolder := IncludeTrailingPathDelimiter(
GetEnvironmentVariable('LOCALAPPDATA')) + 'MyViewer\PageCache';
Pdf.RenderCacheMaxDocuments := 50;
Pdf.RenderCacheMaxBytes := Int64(1024) * 1024 * 1024; // 1 GiB
Pdf.RenderCacheCapacity := 16; // صفحات در حافظه
if Pdf.LoadFromFile(FileName) > 0 then
for I := 0 to Pdf.LoadedPageCount - 1 do
begin
Bmp := Pdf.RenderLoadedPageToBitmapCached(I, 96);
if Bmp <> nil then
try
// کپی را اینجا به نوار thumbnail بده
finally
Bmp.Free; // فراخوانی cacheشده همیشه یک کپی متعلق به فراخواننده برمیگرداند
end;
end;
finally
Pdf.Free; // از v2.770.140 این دیگر مدخلهای دیسکی را حذف نمیکند
end;
end;
همان رویه را دو بار اجرا کن و اجرای دوم هرگز صفحهای را rasterize نمیکند که در کش جا شده باشد. شیء کش دیسکی تنبل روی اولین رندر cacheشده ساخته میشود و تا آزاد شدن instance یعنی THotPDF زندگی میکند، پس عوض کردن RenderCacheFolder یا RenderCacheMaxDocuments یا RenderCacheMaxBytes بعد از آن نقطه یک کش بازشده را جابهجا یا تغییر اندازه نمیدهد. صفحاتی که برای سیاست پذیرش درونحافظهای بزرگاند (بهطور پیشفرض یک مدخل مجاز نیست از 64 مگابایت پیکسل 32 بیتی عبور کند) هم ماندگار نمیشوند، و از لایهٔ دیسک فقط تا وقتی استفاده میشود که RenderFallbackPolicy پیشفرض rfpIgnore اش را نگه دارد، چون تشخیصهای fallback کنار PNG ذخیره نمیشوند
چرا RenderCacheFolder قبل از v2.770.140 هرگز کار نمیکرد؟
RenderCacheFolder قبل از v2.770.140 بیاثر بود چون لایهٔ دیسک اسناد را روی یک hash از بایتهای منبع کلید میزد که loadهای عادی هرگز نگه نمیداشتند. کلید سند از یک SHA-256 روی یک کپی داخلی از بایتهای خام PDF میآمد، اما LoadFromFile و LoadFromStream منبع را درجا parse میکنند و چنین کپیای نگه نمیدارند؛ آن فیلد فقط موقتاً روی یک مسیر بازیابی رمزنگاریشده پر میشد و بلافاصله بعدش پاک میشد. بدون بایت، کلید همیشه خالی بود، و کلید خالی یعنی لایهٔ دیسک دور زده میشود. بدون error، بدون هشدار، فقط یک پوشه که خالی میماند
پر کردن کلید یک باگ دوم را لو داد که پشت اولی پنهان بود. InvalidateRenderedPageCache قدیمی پوشهٔ دیسکی سند را حذف میکرد، و InvalidateRenderedPageCache در شروع هر load و بعد از هر ویرایش و داخل Free اجرا میشود. پس لحظهای که کلید کار میکرد، هر نشست viewer روی خروج کش خودش را نابود میکرد و نشست بعدی هم به هر حال سرد شروع میکرد. بدتر اینکه کلید بعد از یک ویرایش از همان منبع دوباره محاسبه میشد، پس رندرهای سند ویرایششده زیر کلید فایل اصلی ذخیره میشدند و به نشست بعدی که PDF دستنخورده را باز میکرد سرو میشدند. v2.770.140 هویت و بیاعتبارسازی را با هم فیکس میکند؛ فیکس کردن فقط یکی از آنها یا یک کش مرده تحویل میداد یا یک کش دروغگو
HotPDF چطور بدون خواندن کل فایل یک PDF را شناسایی میکند
HotPDF یک PDF که از فایل محلی load شده با یک اثر انگشت از اندازه و زمان آخرین نوشتن و 64 کیلوبایت اول و آخرش شناسایی میکند، و یک منبع استریم یا دسترسیتصادفی را با یک SHA-256 از کل محتوایش. هر دو یک بار موقع موفقیت load گرفته میشوند و 16 نویسهٔ هگز اول خلاصهٔ SHA-256 (64 بیت) کلید سند میشود
| منبع | هویت | هزینه | کی گرفته میشود |
|---|---|---|---|
LoadFromFile | اندازه + LastWriteTime + 64 کیلوبایت اول و آخر، هششده با SHA-256 | حداکثر خواندن 128 کیلوبایت، مستقل از اندازهٔ فایل | هر load موفق، حتی اگر RenderCacheFolder بعداً ست شود |
LoadFromStream | SHA-256 از کل استریم | یک گذر کامل روی منبع | فقط اگر RenderCacheFolder قبل از load ست شده باشد |
LoadFromRandomAccessSource | SHA-256 از کل منبع | یک گذر کامل روی منبع | فقط اگر پوشه اول ست شده باشد و کل بازه در دسترس باشد |
هر منبعی با درایهٔ /Encrypt | هیچ | هیچ | هرگز؛ لایهٔ دیسک دور زده میشود |
اثر انگشت فایل یک معاملهٔ عمدی است. هش کردن کامل یک آرشیو اسکنشدهٔ 400 مگابایتی در هر باز کردن میتواند بیشتر از رندر کردن دو صفحهای که کاربر واقعاً نگاه میکند هزینه بردارد. ناحیههای نمونهبرداریشده تصادفی نیستند: header در ابتدای فایل نشسته و trailer و آخرین بخش cross-reference در انتها (ISO 32000-1 §7.5). یک بهروزرسانی افزایشی یک بدنه و بخش cross-reference و trailer جدید را append میکند (§7.5.6)، پس اندازه و دُم را یکجا عوض میکند. یک بازنویسی کامل توسط هر ابزار عادی زمان آخرین نوشتن را عوض میکند. برای فایلهای تا 128 کیلوبایت دو نمونه هر بایت را میپوشانند، پس اسناد کوچک عملاً کامل هش میشوند
ریسک باقیمانده یک تغییر هماندازه و درجای وسط یک فایل بزرگ است که نویسندهاش بعدش timestamp اصلی را بازیابی کند. آن به ابزاری نیاز دارد که عمداً زمانهای ویرایش را موقع دست زدن به محتوا حفظ کند، که نادر است اما ناممکن نیست، و در آن حالت کش صفحات کهنه را سرو میکند. روی دیگرش خوشخیم است: کپی کردن یک فایل در Windows معمولاً زمان آخرین نوشتن را حفظ میکند، پس کپی سندی که از قبل در کش هست به همان مدخلها میخورد، که درست است چون بایتها یکساناند
استریمها اصلاً زمان ویرایش ندارند، پس تنها هویت صادقانه خود محتواست. HotPDF فقط وقتی برای آن گذر کامل SHA-256 هزینه میدهد که قبل از load یک کش دیسکی خواسته باشی؛ بقیهٔ فراخوانندههای LoadFromStream هیچ هزینهٔ اضافهای نمیبینند. همین ترتیب انتساب property را بارساز میکند:
procedure OpenDownloadedPdf(Pdf: THotPDF; Data: TStream;
const CacheRoot: string);
begin
// ترتیب اشتباه برای استریمها: hash محتوا فقط وقتی محاسبه میشود که
// پوشه از قبل ست شده باشد، پس این سند لایهٔ دیسک را دور میزد
// Pdf.LoadFromStream(Data);
// Pdf.RenderCacheFolder := CacheRoot;
Pdf.RenderCacheFolder := CacheRoot; // اول ست کن
Data.Position := 0;
if Pdf.LoadFromStream(Data) <= 0 then
raise Exception.Create('The stream is not a loadable PDF');
end;
یک منبع دسترسیتصادفی که هنوز در حال دانلود است (بعضی بازهها هنوز در دسترس نیستند) بهجای hash محتوای ناقص هیچ هویتی نمیگیرد، و اگر محاسبهٔ هویت به هر دلیلی شکست بخورد load همچنان موفق میشود؛ سند بهسادگی بدون لایهٔ دیسک رندر میشود
چه چیزی یک مدخل کش دیسکی در HotPDF را بیاعتبار میکند؟
یک مدخل کش دیسکی در HotPDF هرگز با حذف شدن موقع ویرایش بیاعتبار نمیشود؛ در عوض ویرایش سند بارگذاریشده هویت سند را میاندازد، پس لایهٔ دیسک تا آخر آن load دور زده میشود و صفحات ذخیرهشده برای منبع دستنخورده معتبر میمانند. مدخلها فقط از طریق محدودیتهای LRU و بایت، یک PNG خراب، یا تغییر schema دیسک را ترک میکنند
کلید یک منبع روی دیسک را توصیف میکند نه گراف شیء در حافظه را. وقتی روی صفحهای مهر میزنی یا یک حاشیهنویسی را عوض میکنی، سند دیگر با آن منبع نمیخواند، پس نه خواندن و نه نوشتن زیر کلیدش درست نخواهد بود. از v2.770.140 بیاعتبارسازی هم سطح سند هم سطح صفحه بهجای دست زدن به پوشه هویت را پاک میکنند، و یک نگهبان دوم هم برای ویرایشهایی که InvalidateRenderedPageCache را صدا نزدهاند هست: قبل از استفاده از لایهٔ دیسک، THotPDF چک میکند آیا هیچ شیء بارگذاریشدهای dirty است و یک سند dirty را بدون هویت تلقی میکند
تنظیمات رندر برعکس کار میکنند. عوض کردن PageRenderBackend (یا صدا زدن UseNativeGDIRenderBackend) و صدا زدن ConfigureRenderICCWorkflow یا ClearRenderICCWorkflow صفحات درونحافظهای را میشویند اما هویت را نگه میدارند، چون سند همچنان با منبعش میخواند. آن تنظیمات پیکسلها را عوض میکنند بدون اینکه جزو واریانت درونحافظهای باشند، پس کلید دیسکی نام backend و فلگ جبران نقطهٔ سیاه و خلاصههای SHA-256 از پروفایلهای proof و خروجی ICC را در خودش تا میکند. خود واریانت از قبل قصد رنگی و dithering خروجی و پیشنمایش overprint و حالت mask روشنایی و سیاست fallback و دید هر گروه optional content را پوشش میدهد، پس روشن/خاموش کردن یک لایه داخل پوشهٔ متفاوتی رندر میشود بهجای بازنویسی نمای پیشفرض
برای برگرداندن یک سند ویرایششده به لایهٔ دیسک، با ذخیره کردنش و load کردن نتیجه به آن یک هویت منبع جدید بده:
procedure CommitEditsAndRekey(Pdf: THotPDF; const EditedFile: string);
begin
// بعد از ویرایش سند بارگذاریشده: صفحات درونحافظهای را تازه کن.
// هویت منبع از قبل رفته است، پس چیزی از پوشهٔ دیسکی سند اصلی
// خوانده یا نوشته نمیشود
Pdf.InvalidateRenderedPageCache;
// یک فایل ذخیرهشده اندازه و زمان آخرین نوشتن تازه دارد، پس یک
// هویت جدید؛ رندرهای بعد از این load زیر کلید جدید cache میشوند
Pdf.SaveLoadedDocument(EditedFile);
if Pdf.LoadFromFile(EditedFile) <= 0 then
raise Exception.Create('Could not reload the edited document');
end;
پوشهٔ سند اصلی به حال خود رها میشود و مثل هر مدخل دیگری از طریق RenderCacheMaxDocuments و RenderCacheMaxBytes پیر میشود و میرود. اگر کاربر اصل ویرایشنشده را دوباره باز کند، صفحاتش هنوز آنجاست
مرزهای امنیتی: منابع رمزنگاریشده و پوشههای پیوندی
کش رندر دیسکی در HotPDF دو نوع ورودی را عمداً رد میکند: هرگز صفحات یک PDF رمزنگاریشده را روی دیسک نمینویسد و هرگز یک زیرپوشهٔ سند که junction یا نقطهٔ reparse دیگری است دنبال نمیکند. هر دو قاعده hitهای کش را با لو ندادن داده یا حذف نکردن فایلهای اشتباه معامله میکنند
PDFهای رمزنگاریشده هرگز روی دیسک cache نمیشوند
یک صفحهٔ رندرشده محتوای رمزگشاییشده است. نوشتنش بهشکل یک PNG ساده داخل یک پوشهٔ کش یک کپی خواندنی از سند محافظتشده با رمز را روی دیسک جا میگذارد، بیرون از حفاظتی که نویسنده انتخاب کرده (ISO 32000-1 §7.6). برای همین HotPDF برای هر منبعی که trailer اش درایهٔ /Encrypt دارد هویتی نمیگیرد، از جمله فایلهایی که با رمز عبور یا با رمز عبور کاربری خالی باز شدهاند. آن اسناد همچنان از لایهٔ درونحافظهای استفاده میکنند که با فرایند میمیرد
زیرپوشههای junction از v2.770.173 رد میشوند
ریشهٔ کش انتخاب توست و اشاره کردنش به یک junction مجاز است. زیرپوشههای سند زیر آن ماجرا دیگری است: کش آنها را خودش میسازد و میخواند و دست میزند و حذف میکند، در بازیابی شروع به کار (که فایلهای موقت جامانده را پاک میکند) و lookup (که timestampها را بهروز میکند) و ذخیره و بیاعتبارسازی و سه محدودیت اخراج. اگر کسی با دسترسی نوشتن به ریشهٔ کش یک پوشهٔ سند را با یک junction به پوشهٔ دیگری جایگزین کند، هر یک از آن مسیرها دنبالش میرفتند و اخراج فایلهایی را حذف میکرد جای جایی که کش هرگز مالکش نبود. از v2.770.173 هر یک از آن نقاط ورود صفت reparse-point را چک میکند و از پوشهٔ سند پیوندی میگذرد: یک lookup یک miss حساب میکند، یک ذخیره یک شکست نوشتن، و اخراج بیخیالش میشود
مسیرهای Unicode و ریشههای مشترک
دو فیکس مرتبط وقتی اهمیت پیدا میکنند که در پروفایلهای کاربر deploy کنی. قبل از v2.770.135 RenderCacheFolder یک AnsiString بود، پس پوشهای بیرون code page سیستم (مثلاً یک نام کاربری چینی روی یک نصب انگلیسی Windows) قبل از اینکه کش ببیندش lossy تبدیل میشد؛ property حالا یک string یونیکد است و جایگزینی اتمی از Windows API عریض استفاده میکند. از v2.770.52 چندین instance از THotPDF در یک فرایند که به یک ریشه اشاره میکنند (بعد از بسط مسیر، مقایسهشده بدون حساسیت به بزرگی و کوچکی حروف) یک index و یک lock مشترک با شمارندهٔ ارجاع دارند. قبلاً هر instance index.txt را با کپی خودش بازنویسی میکرد و محدودیتها را نسبت به دید ناقصش اعمال میکرد، پس پوشه میتوانست چند برابر از بودجهاش رشد کند
آن اشتراک در مرز فرایند میایستد. دو فرایند جدا روی یک ریشه همچنان indexهای درونحافظهای جدا دارند، پس به هر برنامهای که همزمان اجرا میشود ریشهٔ کش خودش را بده. viewerهایی که روی worker threadها رندر میکنند داخل یک فرایند حالشان خوب است: PrefetchLoadedPages و صفی که در رندر پسزمینه با صف درخواست پوشش داده شده هر دو از همان مسیر cacheشده و همان lock عبور میکنند
مرجع سریع: چکلیست RenderCacheFolder
RenderCacheFolderوRenderCacheMaxDocumentsوRenderCacheMaxBytesرا قبل از اولین فراخوانیRenderLoadedPageToBitmapCachedست کن؛ برای loadهای استریمی و دسترسیتصادفی، پوشه را قبل از load ست کن- اگر به لایهٔ دیسک تکیه داری به v2.770.140 یا بعدتر ارتقا بده؛ نسخههای قبلی property را میپذیرند اما برای loadهای عادی هرگز صفحهای از دیسک سرو نمیکنند
- انتظار کش دیسکی برای PDFهای رمزنگاریشده یا اسناد ویرایششده بعد از load یا وقتی
RenderFallbackPolicyبرابرrfpIgnoreنیست نداشته باش - instance یعنی THotPDF را عادی آزاد کن؛ از v2.770.140 نه
Freeو نهInvalidateRenderedPageCacheمدخلهای دیسکی را حذف نمیکنند - عوض کردن
PageRenderBackendیا گردشکار ICC سند را زیر کلید متفاوتی روی لایهٔ دیسک نگه میدارد - بهازای هر برنامهٔ در حال اجرا یک ریشهٔ کش؛ instanceهای داخل یک فرایند از v2.770.52 ایندکس را شریکاند
- ریشهٔ کش را در یک مکان بهازای هر کاربر نگه دار؛ زیرپوشههای سند که junction هستند از v2.770.173 رد میشوند
یک کش صفحهٔ ماندگار بیشترین بازده را در viewer ای دارد که تمام روز همان اسناد را دوباره باز میکند، که دقیقاً شکل معماری viewer سفارشی PDF در Delphi است که جای دیگری در همین وبلاگ توضیح داده شده. RenderCacheFolder و کش raster درونحافظهای و renderer صفحه با کامپوننت HotPDF Delphi PDF برای Delphi و C++Builder عرضه میشوند