|
| 1 | +// V8 バイトコードキャッシュ。 |
| 2 | +// |
| 3 | +// player+framework のバンドル (数 MB) は毎起動フルパースがかかり、jitless |
| 4 | +// (Ignition) では起動時間の主要因になる。初回起動時に eager コンパイルして |
| 5 | +// バイトコードをディスクへ保存し、2 回目以降はそれを消費してパースを省略する。 |
| 6 | +// 生成物はデータであり動的コード生成ではないため、コンソール (JIT 禁止) でも使える。 |
| 7 | +// |
| 8 | +// - 保存先: %LOCALAPPDATA%\Next2D\codecache\<hash>.v8bc |
| 9 | +// (DomShims の localStorage バックエンドと同じ配置方針。環境変数が無ければ無効化) |
| 10 | +// - キー: スクリプト名 + ソース内容の FNV-1a。ソースが変われば別ファイルになる。 |
| 11 | +// - V8 のバージョン/フラグ不一致は V8 自身が検出して reject する (その場で作り直す)。 |
| 12 | +// - すべてのエラーは graceful degrade (キャッシュ無しの通常コンパイルに落ちる)。 |
| 13 | +// - 小さいスクリプトはパースが安く I/O の方が高いため対象外 (kMinSourceSize)。 |
| 14 | +#pragma once |
| 15 | + |
| 16 | +#include <v8.h> |
| 17 | + |
| 18 | +#include <chrono> |
| 19 | +#include <cstdint> |
| 20 | +#include <cstdio> |
| 21 | +#include <cstdlib> |
| 22 | +#include <cstring> |
| 23 | +#include <filesystem> |
| 24 | +#include <fstream> |
| 25 | +#include <memory> |
| 26 | +#include <string> |
| 27 | +#include <vector> |
| 28 | + |
| 29 | +namespace next2d::codecache { |
| 30 | + |
| 31 | +// キャッシュ対象の最小ソースサイズ。bootstrap.js (十数 KB) は対象外、 |
| 32 | +// ゲーム/player バンドル (数百 KB〜数 MB) が対象になる境界。 |
| 33 | +inline constexpr size_t kMinSourceSize = 64 * 1024; |
| 34 | + |
| 35 | +inline const std::filesystem::path& CacheDir() |
| 36 | +{ |
| 37 | + static const std::filesystem::path dir = []() -> std::filesystem::path { |
| 38 | + const char* base = std::getenv("LOCALAPPDATA"); |
| 39 | + if (!base || !*base) { |
| 40 | + return {}; |
| 41 | + } |
| 42 | + std::error_code ec; |
| 43 | + std::filesystem::path d = std::filesystem::path(base) / "Next2D" / "codecache"; |
| 44 | + std::filesystem::create_directories(d, ec); |
| 45 | + if (ec) { |
| 46 | + return {}; |
| 47 | + } |
| 48 | + // ゲーム更新でソースが変わるとキャッシュはファイル名ごと変わり、旧ファイルは |
| 49 | + // 参照されない孤児になる。30 日アクセスの無い .v8bc を起動時に掃除する |
| 50 | + // (Load が使用時に write time を touch するため実質 LRU)。 |
| 51 | + const auto now = std::filesystem::file_time_type::clock::now(); |
| 52 | + std::error_code iter_ec; |
| 53 | + for (const auto& entry : std::filesystem::directory_iterator(d, iter_ec)) { |
| 54 | + std::error_code e2; |
| 55 | + if (entry.path().extension() != ".v8bc") { |
| 56 | + continue; |
| 57 | + } |
| 58 | + const auto t = std::filesystem::last_write_time(entry.path(), e2); |
| 59 | + if (!e2 && now - t > std::chrono::hours(24 * 30)) { |
| 60 | + std::filesystem::remove(entry.path(), e2); |
| 61 | + } |
| 62 | + } |
| 63 | + return d; |
| 64 | + }(); |
| 65 | + return dir; |
| 66 | +} |
| 67 | + |
| 68 | +inline uint64_t Fnv1a(const void* data, size_t size, uint64_t hash = 1469598103934665603ull) |
| 69 | +{ |
| 70 | + const auto* p = static_cast<const unsigned char*>(data); |
| 71 | + for (size_t i = 0; i < size; ++i) { |
| 72 | + hash ^= p[i]; |
| 73 | + hash *= 1099511628211ull; |
| 74 | + } |
| 75 | + return hash; |
| 76 | +} |
| 77 | + |
| 78 | +inline std::filesystem::path CacheFileFor(const std::string& name, const std::string& source) |
| 79 | +{ |
| 80 | + const std::filesystem::path& dir = CacheDir(); |
| 81 | + if (dir.empty()) { |
| 82 | + return {}; |
| 83 | + } |
| 84 | + uint64_t h = Fnv1a(name.data(), name.size()); |
| 85 | + h = Fnv1a(source.data(), source.size(), h); |
| 86 | + char buf[17] = {}; |
| 87 | + std::snprintf(buf, sizeof(buf), "%016llx", static_cast<unsigned long long>(h)); |
| 88 | + return dir / (std::string(buf) + ".v8bc"); |
| 89 | +} |
| 90 | + |
| 91 | +inline bool Eligible(const std::string& source) |
| 92 | +{ |
| 93 | + return source.size() >= kMinSourceSize && !CacheDir().empty(); |
| 94 | +} |
| 95 | + |
| 96 | +// キャッシュファイルを読む。戻り値の所有権は ScriptCompiler::Source が取る |
| 97 | +// (BufferOwned なので CachedData 破棄時に delete[] される)。無ければ nullptr。 |
| 98 | +inline v8::ScriptCompiler::CachedData* Load(const std::string& name, const std::string& source) |
| 99 | +{ |
| 100 | + const std::filesystem::path file = CacheFileFor(name, source); |
| 101 | + if (file.empty()) { |
| 102 | + return nullptr; |
| 103 | + } |
| 104 | + std::ifstream ifs(file, std::ios::binary); |
| 105 | + if (!ifs) { |
| 106 | + return nullptr; |
| 107 | + } |
| 108 | + std::vector<char> buf( |
| 109 | + (std::istreambuf_iterator<char>(ifs)), std::istreambuf_iterator<char>()); |
| 110 | + if (buf.empty()) { |
| 111 | + return nullptr; |
| 112 | + } |
| 113 | + // 使用時に write time を更新し、CacheDir の 30 日掃除を LRU として機能させる |
| 114 | + std::error_code ec; |
| 115 | + std::filesystem::last_write_time( |
| 116 | + file, std::filesystem::file_time_type::clock::now(), ec); |
| 117 | + |
| 118 | + auto* data = new uint8_t[buf.size()]; |
| 119 | + std::memcpy(data, buf.data(), buf.size()); |
| 120 | + return new v8::ScriptCompiler::CachedData( |
| 121 | + data, static_cast<int>(buf.size()), |
| 122 | + v8::ScriptCompiler::CachedData::BufferOwned); |
| 123 | +} |
| 124 | + |
| 125 | +inline void Store(const std::string& name, const std::string& source, |
| 126 | + const v8::ScriptCompiler::CachedData* data) |
| 127 | +{ |
| 128 | + if (!data || !data->data || data->length <= 0) { |
| 129 | + return; |
| 130 | + } |
| 131 | + const std::filesystem::path file = CacheFileFor(name, source); |
| 132 | + if (file.empty()) { |
| 133 | + return; |
| 134 | + } |
| 135 | + std::ofstream ofs(file, std::ios::binary | std::ios::trunc); |
| 136 | + if (ofs) { |
| 137 | + ofs.write(reinterpret_cast<const char*>(data->data), data->length); |
| 138 | + } |
| 139 | +} |
| 140 | + |
| 141 | +// classic script をキャッシュ込みでコンパイルする。 |
| 142 | +// - キャッシュ有り: kConsumeCodeCache で消費 (V8 が reject したら作り直して保存) |
| 143 | +// - キャッシュ無し: kEagerCompile で全関数を一括コンパイルして保存 |
| 144 | +// (初回はその分遅いが、以降の起動は lazy compile も含めて丸ごと省ける) |
| 145 | +// - 対象外サイズ: 従来どおりの lazy コンパイル |
| 146 | +inline v8::MaybeLocal<v8::Script> Compile(v8::Local<v8::Context> ctx, |
| 147 | + v8::Local<v8::String> src, |
| 148 | + v8::ScriptOrigin& origin, |
| 149 | + const std::string& name, |
| 150 | + const std::string& source_str) |
| 151 | +{ |
| 152 | + if (!Eligible(source_str)) { |
| 153 | + v8::ScriptCompiler::Source source(src, origin); |
| 154 | + return v8::ScriptCompiler::Compile(ctx, &source); |
| 155 | + } |
| 156 | + |
| 157 | + if (v8::ScriptCompiler::CachedData* cached = Load(name, source_str)) { |
| 158 | + v8::ScriptCompiler::Source source(src, origin, cached); |
| 159 | + v8::Local<v8::Script> script; |
| 160 | + if (!v8::ScriptCompiler::Compile( |
| 161 | + ctx, &source, v8::ScriptCompiler::kConsumeCodeCache).ToLocal(&script)) { |
| 162 | + return {}; |
| 163 | + } |
| 164 | + if (source.GetCachedData() && source.GetCachedData()->rejected) { |
| 165 | + std::unique_ptr<v8::ScriptCompiler::CachedData> fresh( |
| 166 | + v8::ScriptCompiler::CreateCodeCache(script->GetUnboundScript())); |
| 167 | + Store(name, source_str, fresh.get()); |
| 168 | + } |
| 169 | + return script; |
| 170 | + } |
| 171 | + |
| 172 | + v8::ScriptCompiler::Source source(src, origin); |
| 173 | + v8::Local<v8::Script> script; |
| 174 | + if (!v8::ScriptCompiler::Compile( |
| 175 | + ctx, &source, v8::ScriptCompiler::kEagerCompile).ToLocal(&script)) { |
| 176 | + return {}; |
| 177 | + } |
| 178 | + std::unique_ptr<v8::ScriptCompiler::CachedData> fresh( |
| 179 | + v8::ScriptCompiler::CreateCodeCache(script->GetUnboundScript())); |
| 180 | + Store(name, source_str, fresh.get()); |
| 181 | + return script; |
| 182 | +} |
| 183 | + |
| 184 | +// ES module 版。規約は Compile と同じ。 |
| 185 | +inline v8::MaybeLocal<v8::Module> CompileModule(v8::Isolate* isolate, |
| 186 | + v8::Local<v8::String> src, |
| 187 | + v8::ScriptOrigin& origin, |
| 188 | + const std::string& name, |
| 189 | + const std::string& source_str) |
| 190 | +{ |
| 191 | + if (!Eligible(source_str)) { |
| 192 | + v8::ScriptCompiler::Source source(src, origin); |
| 193 | + return v8::ScriptCompiler::CompileModule(isolate, &source); |
| 194 | + } |
| 195 | + |
| 196 | + if (v8::ScriptCompiler::CachedData* cached = Load(name, source_str)) { |
| 197 | + v8::ScriptCompiler::Source source(src, origin, cached); |
| 198 | + v8::Local<v8::Module> module; |
| 199 | + if (!v8::ScriptCompiler::CompileModule( |
| 200 | + isolate, &source, v8::ScriptCompiler::kConsumeCodeCache).ToLocal(&module)) { |
| 201 | + return {}; |
| 202 | + } |
| 203 | + if (source.GetCachedData() && source.GetCachedData()->rejected) { |
| 204 | + std::unique_ptr<v8::ScriptCompiler::CachedData> fresh( |
| 205 | + v8::ScriptCompiler::CreateCodeCache(module->GetUnboundModuleScript())); |
| 206 | + Store(name, source_str, fresh.get()); |
| 207 | + } |
| 208 | + return module; |
| 209 | + } |
| 210 | + |
| 211 | + v8::ScriptCompiler::Source source(src, origin); |
| 212 | + v8::Local<v8::Module> module; |
| 213 | + if (!v8::ScriptCompiler::CompileModule( |
| 214 | + isolate, &source, v8::ScriptCompiler::kEagerCompile).ToLocal(&module)) { |
| 215 | + return {}; |
| 216 | + } |
| 217 | + std::unique_ptr<v8::ScriptCompiler::CachedData> fresh( |
| 218 | + v8::ScriptCompiler::CreateCodeCache(module->GetUnboundModuleScript())); |
| 219 | + Store(name, source_str, fresh.get()); |
| 220 | + return module; |
| 221 | +} |
| 222 | + |
| 223 | +} // namespace next2d::codecache |
0 commit comments