• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ossia / score / 30157002862

25 Jul 2026 11:53AM UTC coverage: 15.406% (+0.1%) from 15.311%
30157002862

Pull #2119

github

web-flow
Merge 51e0e1b89 into 13afd939a
Pull Request #2119: gfx: caching + offscreen infrastructure (pre-scene, from #2109)

247 of 597 new or added lines in 12 files covered. (41.37%)

9 existing lines in 4 files now uncovered.

30668 of 199059 relevant lines covered (15.41%)

981.72 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.41
/src/plugins/score-plugin-gfx/Gfx/AssetTable.cpp
1
#include <Gfx/AssetTable.hpp>
2

3
#include <cstddef>
4
#include <cstdint>
5

6
namespace Gfx
7
{
8

9
namespace
10
{
11
std::size_t estimateSize(const AssetTable::DecodedAsset& a) noexcept
34✔
12
{
13
  std::size_t total = 0;
34✔
14
  if(!a.image.isNull())
34✔
15
    total += static_cast<std::size_t>(a.image.sizeInBytes());
28✔
16
  if(a.bytes)
34✔
17
    total += a.bytes->size();
5✔
18
  return total;
34✔
19
}
20
}
21

22
void AssetTable::stage(uint64_t content_hash, QImage image)
30✔
23
{
24
  std::lock_guard lock{m_mutex};
30✔
25
  auto it = m_entries.find(content_hash);
30✔
26
  if(it != m_entries.end())
30✔
27
    return; // Hash contract: same hash = same bytes. Idempotent stage.
1✔
28

29
  auto e = std::make_shared<DecodedAsset>();
29✔
30
  e->image = std::move(image);
29✔
31
  e->byte_size = estimateSize(*e);
29✔
32
  const std::size_t sz = e->byte_size;
29✔
33
  m_total_bytes += sz;
29✔
34

35
  Slot s;
29✔
36
  s.asset = std::move(e);
29✔
37
  // A freshly-staged entry has refcount 0 and no holder yet. Put it in the
38
  // cold LRU immediately so that a stage() whose consumer never acquire()s it
39
  // (material removed / model swapped / undo before upload) is still trimmable;
40
  // otherwise it would be counted in m_total_bytes forever with no path to
41
  // eviction. acquire() splices it back out of the cold pool on first use.
42
  m_lru.push_front(content_hash);
29✔
43
  s.lru_it = m_lru.begin();
29✔
44
  s.in_lru = true;
29✔
45
  m_cold_bytes += sz;
29✔
46
  m_entries.emplace(content_hash, std::move(s));
29✔
47
}
30✔
48

49
void AssetTable::stage(
7✔
50
    uint64_t content_hash,
51
    std::shared_ptr<const std::vector<uint8_t>> bytes,
52
    std::string mime_type)
53
{
54
  std::lock_guard lock{m_mutex};
7✔
55
  auto it = m_entries.find(content_hash);
7✔
56
  if(it != m_entries.end())
7✔
57
    return;
2✔
58

59
  auto e = std::make_shared<DecodedAsset>();
5✔
60
  e->bytes = std::move(bytes);
5✔
61
  e->mime_type = std::move(mime_type);
5✔
62
  e->byte_size = estimateSize(*e);
5✔
63
  const std::size_t sz = e->byte_size;
5✔
64
  m_total_bytes += sz;
5✔
65

66
  Slot s;
5✔
67
  s.asset = std::move(e);
5✔
68
  // See the QImage stage() overload: staged-but-never-acquired entries must be
69
  // reclaimable, so seed them into the cold LRU. acquire() removes them again.
70
  m_lru.push_front(content_hash);
5✔
71
  s.lru_it = m_lru.begin();
5✔
72
  s.in_lru = true;
5✔
73
  m_cold_bytes += sz;
5✔
74
  m_entries.emplace(content_hash, std::move(s));
5✔
75
}
7✔
76

77
std::shared_ptr<const AssetTable::DecodedAsset>
78
AssetTable::acquire(uint64_t content_hash)
32✔
79
{
80
  std::lock_guard lock{m_mutex};
32✔
81
  auto it = m_entries.find(content_hash);
32✔
82
  if(it == m_entries.end())
32✔
83
    return {};
10✔
84
  auto& slot = it->second;
22✔
85

86
  // Resurrect from LRU if cold.
87
  if(slot.in_lru)
22✔
88
  {
89
    m_lru.erase(slot.lru_it);
19✔
90
    slot.in_lru = false;
19✔
91
    m_cold_bytes -= slot.asset->byte_size;
19✔
92
  }
19✔
93

94
  ++slot.asset->refcount;
22✔
95
  return slot.asset;
22✔
96
}
32✔
97

98
std::shared_ptr<const AssetTable::DecodedAsset>
99
AssetTable::peek(uint64_t content_hash) const
3✔
100
{
101
  std::lock_guard lock{m_mutex};
3✔
102
  auto it = m_entries.find(content_hash);
3✔
103
  if(it == m_entries.end())
3✔
104
    return {};
1✔
105
  // Intentionally does NOT move out of LRU nor bump refcount — the
106
  // caller just wants a read-through. If the entry is cold it stays
107
  // cold (still evictable next trim). shared_ptr semantics keep the
108
  // DecodedAsset alive as long as the caller holds the returned ptr,
109
  // even if eviction happens concurrently on another thread.
110
  return it->second.asset;
2✔
111
}
3✔
112

113
void AssetTable::release(uint64_t content_hash)
11✔
114
{
115
  std::lock_guard lock{m_mutex};
11✔
116
  auto it = m_entries.find(content_hash);
11✔
117
  if(it == m_entries.end())
11✔
118
    return;
1✔
119
  auto& slot = it->second;
10✔
120
  if(slot.asset->refcount > 0)
10✔
121
    --slot.asset->refcount;
8✔
122
  if(slot.asset->refcount == 0 && !slot.in_lru)
10✔
123
  {
124
    // Newest-first: push_front, tail is oldest. trim() pops from tail.
125
    m_lru.push_front(content_hash);
7✔
126
    slot.lru_it = m_lru.begin();
7✔
127
    slot.in_lru = true;
7✔
128
    m_cold_bytes += slot.asset->byte_size;
7✔
129
  }
7✔
130
}
11✔
131

132
void AssetTable::evictOne() noexcept
10✔
133
{
134
  // Caller holds m_mutex.
135
  if(m_lru.empty())
10✔
NEW
136
    return;
×
137
  const uint64_t hash = m_lru.back();
10✔
138
  m_lru.pop_back();
10✔
139

140
  auto it = m_entries.find(hash);
10✔
141
  if(it == m_entries.end())
10✔
NEW
142
    return;
×
143

144
  const std::size_t sz = it->second.asset->byte_size;
10✔
145
  m_total_bytes -= sz;
10✔
146
  m_cold_bytes -= sz;
10✔
147
  m_entries.erase(it);
10✔
148
}
10✔
149

150
std::size_t AssetTable::trim(std::size_t max_bytes_budget)
9✔
151
{
152
  std::lock_guard lock{m_mutex};
9✔
153
  std::size_t evicted = 0;
9✔
154
  // Only evict from cold pool — hot entries stay regardless of budget.
155
  while(m_cold_bytes > max_bytes_budget && !m_lru.empty())
16✔
156
  {
157
    const std::size_t before_total = m_total_bytes;
7✔
158
    evictOne();
7✔
159
    evicted += (before_total - m_total_bytes);
7✔
160
  }
161
  return evicted;
9✔
162
}
9✔
163

164
void AssetTable::maybeAutoTrim(
5✔
165
    float utilization, float high_watermark, float target)
166
{
167
  if(utilization < high_watermark)
5✔
168
    return;
2✔
169

170
  std::lock_guard lock{m_mutex};
3✔
171
  if(m_cold_bytes == 0)
3✔
172
    return;
1✔
173

174
  // Convert target utilization to a cold-pool budget. Heuristic:
175
  // scale the current cold pool by (target / utilization). At
176
  // util=0.85, target=0.60 → trim to ~70% of current cold total.
177
  // Not a proper memory-pressure solver — a low-cost knob that
178
  // kicks in on sustained overload.
179
  const float scale = target / utilization;
2✔
180
  const auto budget
2✔
181
      = static_cast<std::size_t>(static_cast<float>(m_cold_bytes) * scale);
2✔
182
  while(m_cold_bytes > budget && !m_lru.empty())
5✔
183
    evictOne();
3✔
184
}
5✔
185

186
std::size_t AssetTable::size() const noexcept
25✔
187
{
188
  std::lock_guard lock{m_mutex};
25✔
189
  return m_entries.size();
25✔
190
}
25✔
191

192
std::size_t AssetTable::totalBytes() const noexcept
15✔
193
{
194
  std::lock_guard lock{m_mutex};
15✔
195
  return m_total_bytes;
15✔
196
}
15✔
197

198
std::size_t AssetTable::coldCount() const noexcept
21✔
199
{
200
  std::lock_guard lock{m_mutex};
21✔
201
  return m_lru.size();
21✔
202
}
21✔
203

204
} // namespace Gfx
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc