Skip to content

Commit 8f18883

Browse files
committed
add: Implement frame memory cache with LRU eviction
- Add FrameMemoryCache class with cross-platform memory detection - Implement LRU thread for automatic cache eviction based on memory budget - Integrate memory cache into existing frame caching system - Update FrameHashCache to support override methods for memory cache - Add platform-specific memory detection for Windows, Linux, and macOS - Maintain cache consistency with timestamp-based frame versioning
1 parent 3c11aea commit 8f18883

7 files changed

Lines changed: 412 additions & 17 deletions

File tree

TODO-zh.md

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,6 @@
1515

1616
## 小步快跑计划(LRU)
1717

18-
### 步骤 0:先清基线
19-
- 删除未接入运行时路径的原型文件:
20-
- `app/render/cache/framecache.h`
21-
- `app/render/cache/framecache.cpp`
22-
- `app/render/cache/framediskcache.h`
23-
- `app/render/cache/framediskcache.cpp`
24-
- `app/render/cache/framememcache.h`
25-
- 先确保工程可编译,且现有磁盘缓存行为不变。
26-
2718
### 步骤 1:新增最小可用内存 LRU 容器
2819
- 新增 `app/render/cache/framememorycache.h/.cpp`
2920
- `FrameMemoryCacheKey``cache_uuid + timestamp + video params 签名`

app/render/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ set(OLIVE_SOURCES
3232
render/diskmanager.cpp
3333
render/diskmanager.h
3434
render/framehashcache.cpp
35+
render/framememorycache.h
36+
render/framememorycache.cpp
3537
render/framehashcache.h
3638
render/framemanager.cpp
3739
render/framemanager.h

app/render/framehashcache.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ QString FrameHashCache::GetValidCacheFilename(const rational &time) const
8484
return QString();
8585
}
8686

87-
bool FrameHashCache::SaveCacheFrame(const int64_t &time, FramePtr frame) const
87+
bool FrameHashCache::SaveCacheFrame(const int64_t &time, FramePtr frame)
8888
{
8989
return SaveCacheFrame(GetCacheDirectory(), GetUuid(), time, frame);
9090
}

app/render/framehashcache.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,15 +52,15 @@ class FrameHashCache : public PlaybackCache {
5252
QString GetValidCacheFilename(const rational &time) const;
5353

5454
static bool SaveCacheFrame(const QString &filename, FramePtr frame);
55-
bool SaveCacheFrame(const int64_t &time, FramePtr frame) const;
55+
bool SaveCacheFrame(const int64_t &time, FramePtr frame) override;
5656
static bool SaveCacheFrame(const QString &cache_path, const QUuid &uuid,
5757
const int64_t &time, FramePtr frame);
5858
static bool SaveCacheFrame(const QString &cache_path, const QUuid &uuid,
5959
const rational &time, const rational &tb,
6060
FramePtr frame);
6161
static FramePtr LoadCacheFrame(const QString &cache_path, const QUuid &uuid,
6262
const int64_t &time);
63-
FramePtr LoadCacheFrame(const int64_t &time) const;
63+
FramePtr LoadCacheFrame(const int64_t &time) const override;
6464
static FramePtr LoadCacheFrame(const QString &fn);
6565

6666
virtual void SetPassthrough(PlaybackCache *cache) override;

app/render/framememorycache.cpp

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
/*
2+
* Oak Video Editor - Non-Linear Video Editor
3+
* Copyright (C) 2025 Olive CE Team
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*
18+
*/
19+
#include "framememorycache.h"
20+
#include "codec/frame.h"
21+
#include <algorithm>
22+
#include <chrono>
23+
#include <cstdint>
24+
#include <fstream>
25+
#include <limits>
26+
#include <string>
27+
#include <QMutexLocker>
28+
29+
#if defined(_WIN32)
30+
#include <windows.h>
31+
#elif defined(__APPLE__)
32+
#include <mach/mach.h>
33+
#endif
34+
35+
using namespace olive;
36+
37+
QHash<FrameMemCacheKey, FrameMemCacheValue> FrameMemCache::cache_;
38+
QMutex FrameMemCache::cache_mutex_;
39+
std::thread FrameMemCache::lru_thread_;
40+
std::atomic<bool> FrameMemCache::lru_thread_running_(false);
41+
std::atomic<bool> FrameMemCache::lru_thread_stop_requested_(false);
42+
std::atomic<int> FrameMemCache::instance_count_(0);
43+
std::atomic<int64_t> FrameMemCache::budget(0);
44+
45+
namespace {
46+
47+
[[maybe_unused]] uint64_t GetAvailableMemoryBytesWindows()
48+
{
49+
#if defined(_WIN32)
50+
MEMORYSTATUSEX status;
51+
status.dwLength = sizeof(status);
52+
if (!GlobalMemoryStatusEx(&status)) {
53+
return 0;
54+
}
55+
return static_cast<uint64_t>(status.ullAvailPhys);
56+
#else
57+
return 0;
58+
#endif
59+
}
60+
61+
[[maybe_unused]] uint64_t GetAvailableMemoryBytesLinux()
62+
{
63+
#if defined(__linux__)
64+
std::ifstream meminfo("/proc/meminfo");
65+
if (!meminfo.is_open()) {
66+
return 0;
67+
}
68+
69+
std::string key;
70+
uint64_t value_kb = 0;
71+
std::string unit;
72+
uint64_t mem_free_kb = 0;
73+
uint64_t buffers_kb = 0;
74+
uint64_t cached_kb = 0;
75+
76+
while (meminfo >> key >> value_kb >> unit) {
77+
if (key == "MemAvailable:") {
78+
return value_kb * 1024ULL;
79+
}
80+
if (key == "MemFree:") {
81+
mem_free_kb = value_kb;
82+
} else if (key == "Buffers:") {
83+
buffers_kb = value_kb;
84+
} else if (key == "Cached:") {
85+
cached_kb = value_kb;
86+
}
87+
}
88+
89+
// Fallback for older kernels where MemAvailable is not present.
90+
return (mem_free_kb + buffers_kb + cached_kb) * 1024ULL;
91+
#else
92+
return 0;
93+
#endif
94+
}
95+
96+
[[maybe_unused]] uint64_t GetAvailableMemoryBytesMacOS()
97+
{
98+
#if defined(__APPLE__)
99+
mach_port_t host = mach_host_self();
100+
vm_size_t page_size = 0;
101+
if (host_page_size(host, &page_size) != KERN_SUCCESS) {
102+
return 0;
103+
}
104+
105+
vm_statistics64_data_t vm_stats;
106+
mach_msg_type_number_t count = HOST_VM_INFO64_COUNT;
107+
if (host_statistics64(host, HOST_VM_INFO64,
108+
reinterpret_cast<host_info64_t>(&vm_stats),
109+
&count) != KERN_SUCCESS) {
110+
return 0;
111+
}
112+
113+
return static_cast<uint64_t>(vm_stats.free_count) * static_cast<uint64_t>(page_size);
114+
#else
115+
return 0;
116+
#endif
117+
}
118+
119+
[[maybe_unused]] uint64_t GetAvailableMemoryBytes()
120+
{
121+
#if defined(_WIN32)
122+
return GetAvailableMemoryBytesWindows();
123+
#elif defined(__linux__)
124+
return GetAvailableMemoryBytesLinux();
125+
#elif defined(__APPLE__)
126+
return GetAvailableMemoryBytesMacOS();
127+
#else
128+
return 0;
129+
#endif
130+
}
131+
132+
} // namespace
133+
134+
FramePtr FrameMemCache::LoadCacheFrame(const int64_t &time) const{
135+
QMutexLocker locker(&cache_mutex_);
136+
137+
FramePtr result = nullptr;
138+
std::time_t newest_timestamp = 0;
139+
const QUuid cache_uuid = GetUuid();
140+
141+
for (auto it = cache_.cbegin(); it != cache_.cend(); ++it) {
142+
if (it.key().uuid() == cache_uuid && it.key().frame_time() == time) {
143+
if (!result || it.key().timestamp() >= newest_timestamp) {
144+
newest_timestamp = it.key().timestamp();
145+
result = it.value().frame();
146+
}
147+
}
148+
}
149+
150+
return result;
151+
}
152+
153+
bool FrameMemCache::SaveCacheFrame(const int64_t &time, FramePtr frame)
154+
{
155+
if (!frame) {
156+
return false;
157+
}
158+
159+
const FrameMemCacheKey key =
160+
FrameMemCacheKey::create(GetUuid(), time, frame->video_params());
161+
{
162+
QMutexLocker locker(&cache_mutex_);
163+
cache_.insert(key, FrameMemCacheValue::create(frame));
164+
}
165+
166+
doLru();
167+
return true;
168+
}
169+
170+
FrameMemCache::FrameMemCache()
171+
{
172+
if (instance_count_.fetch_add(1, std::memory_order_acq_rel) == 0) {
173+
StartLruThread();
174+
}
175+
}
176+
177+
FrameMemCache::~FrameMemCache()
178+
{
179+
if (instance_count_.fetch_sub(1, std::memory_order_acq_rel) == 1) {
180+
StopLruThread();
181+
}
182+
}
183+
184+
void FrameMemCache::StartLruThread()
185+
{
186+
if (lru_thread_running_.exchange(true, std::memory_order_acq_rel)) {
187+
return;
188+
}
189+
190+
lru_thread_stop_requested_.store(false, std::memory_order_release);
191+
lru_thread_ = std::thread(&FrameMemCache::LruWorkerLoop);
192+
}
193+
194+
void FrameMemCache::StopLruThread()
195+
{
196+
lru_thread_stop_requested_.store(true, std::memory_order_release);
197+
198+
if (lru_thread_.joinable()) {
199+
lru_thread_.join();
200+
}
201+
202+
lru_thread_running_.store(false, std::memory_order_release);
203+
}
204+
205+
void FrameMemCache::LruWorkerLoop()
206+
{
207+
using namespace std::chrono_literals;
208+
209+
int tick_count = 0;
210+
211+
while (!lru_thread_stop_requested_.load(std::memory_order_acquire)) {
212+
if (tick_count % 5 == 0) {
213+
const uint64_t available_bytes = GetAvailableMemoryBytes();
214+
if (available_bytes > 0) {
215+
const uint64_t budget_bytes = available_bytes * 3ULL / 10ULL;
216+
const uint64_t clamped = std::min<uint64_t>(
217+
budget_bytes, static_cast<uint64_t>(std::numeric_limits<int64_t>::max()));
218+
budget.store(static_cast<int64_t>(clamped), std::memory_order_release);
219+
}
220+
}
221+
222+
doLru();
223+
++tick_count;
224+
std::this_thread::sleep_for(1s);
225+
}
226+
}
227+
228+
void FrameMemCache::doLru(){
229+
QMutexLocker locker(&cache_mutex_);
230+
231+
if (cache_.isEmpty()) {
232+
return;
233+
}
234+
235+
const int64_t configured_budget = budget.load(std::memory_order_acquire);
236+
if (configured_budget <= 0) {
237+
return;
238+
}
239+
240+
uint64_t total_bytes = 0;
241+
for (auto it = cache_.begin(); it != cache_.end(); ++it) {
242+
total_bytes += static_cast<uint64_t>(it.value().size_bytes());
243+
}
244+
245+
const uint64_t target_budget = static_cast<uint64_t>(configured_budget);
246+
247+
if (total_bytes <= target_budget) {
248+
return;
249+
}
250+
251+
while (!cache_.isEmpty() && total_bytes > target_budget) {
252+
auto oldest_it = cache_.begin();
253+
std::time_t oldest_ts = oldest_it.key().timestamp();
254+
255+
for (auto it = cache_.begin(); it != cache_.end(); ++it) {
256+
const std::time_t current_ts = it.key().timestamp();
257+
if (current_ts < oldest_ts) {
258+
oldest_ts = current_ts;
259+
oldest_it = it;
260+
}
261+
}
262+
263+
total_bytes -=
264+
std::min(total_bytes, static_cast<uint64_t>(oldest_it.value().size_bytes()));
265+
cache_.erase(oldest_it);
266+
}
267+
}

0 commit comments

Comments
 (0)