Skip to content

Commit 6a758a2

Browse files
fix: Actually fix JavaScriptTimerAction invocation crash
Previous fix was a red herring. Whilst technically there was an issue there, it turned out not the be the cause of the crash that I've repeatedly ran into. The real issue was that SArray isn't really a safe data structure, by design. Smaller revision ranges exacerbate the data structure's limitations. For some use cases a small revision range will be fine, but timers are likely a prime example of an API where a small revision range is NOT fine. This is because the way timers are used in JavaScript is often as timeouts, which is why the API is called setTimeout(). Often the user schedules a timeout and it's never expected to fire. Instead clearTimeout will be called when some other operation returns before the timeout is reached. This means timers are frequently being inserted and deleted. SArray by design will reuse indexes, when this occurs the revision is incremented. However, the Index32 data structure only permits 64 revisions. That number is likely to (and in my case definitely does) wrap around frequently. That on its lonesome isn't an issue. What's problematic is that the timer manager uses an efficient wheel data structure built atop SArray. Scheduled timers are added to one of many wheels and into the SArray index of all scheduled timers. When a timer is cleared, the wheels are not touched at all, only the used timer SArray is modified. The idea being that later on when it's time to execute the relevant timers stored in (part of) a wheel, we just check to see if index in the wheel resolves to a timer in our used/scheduled timer SArray. We a small revision range, we frequently end up reusing both and index AND matching revision. This leads to both early firing of timers AND firing of timers that have since been destructed i.e. cause the crash. This commit swap from using 32-bit index+revisions to 52-bit index+revisions. 20 bits of which are reserved for the revision. So now we can have over 1 million revisions before wrapping around. It's now substantially unlikely for index re-use to happen. HOWEVER, in theory, it COULD still occur. Suppose that in this extremely unlikely circumstance our timer were to fire too early. That's not ideal, but is probably a risk we can live with. What we can't accept is the risk of an outright crash. So we've also implemented protection in JavaScriptTimerAction itself. Basically its destructor now effectively marks itself as being destroyed. The execution logic detects this and bails out, evading the crash.
1 parent eedd327 commit 6a758a2

File tree

6 files changed

+44
-29
lines changed

6 files changed

+44
-29
lines changed

bridge/jsb_essentials.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,8 @@ namespace jsb
167167
Environment::wrap(isolate)->get_timer_manager().set_timer(handle,
168168
JavaScriptTimerAction(v8::Global<v8::Function>(isolate, func), 0), rate > 0 ? rate : 0, loop);
169169
}
170-
info.GetReturnValue().Set((int32_t) handle);
170+
// TODO: V8 update. Once we update V8 past 12.6.221 we can skip the cast to double
171+
info.GetReturnValue().Set((double) (int64_t) handle);
171172
}
172173

173174
void _clear_timer(const v8::FunctionCallbackInfo<v8::Value>& info)

bridge/jsb_timer_action.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ namespace jsb
66
{
77
void JavaScriptTimerAction::operator()(v8::Isolate* isolate)
88
{
9-
jsb_checkf(function_, "Attempt made to call a moved/unassigned JavaScriptTimerAction");
9+
if (!function_)
10+
{
11+
JSB_LOG(Warning, "Ignored attempt to execute a unassigned/moved/destroyed JavaScriptTimerAction");
12+
return;
13+
}
1014

1115
const v8::Local<v8::Function> func = function_->Get(isolate);
1216
const v8::Local<v8::Context> context = func->GetCreationContextChecked();

bridge/jsb_timer_action.h

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

66
namespace jsb
77
{
8+
/**
9+
* This struct is *not* POD, but aims to be compatible with SArray's memory relocation logic.
10+
*/
811
struct JavaScriptTimerAction
912
{
1013
jsb_force_inline JavaScriptTimerAction(): function_(nullptr), argc_(0), argv_(nullptr)
@@ -29,6 +32,8 @@ namespace jsb
2932
{
3033
delete function_;
3134
delete[] argv_;
35+
function_ = nullptr;
36+
argv_ = nullptr;
3237
}
3338

3439
JavaScriptTimerAction(JavaScriptTimerAction& p_other) = delete;

internal/jsb_format.h

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@
66

77
namespace jsb::internal
88
{
9-
template<typename T> struct TFormat { static Variant from(const T& p_item) { return p_item; } };
10-
template<> struct TFormat<Index64> { static Variant from(const Index64& p_item) { return *p_item; } };
11-
template<> struct TFormat<Index32> { static Variant from(const Index32& p_item) { return *p_item; } };
12-
template<> struct TFormat<uintptr_t> { static Variant from(const uintptr_t& p_item) { return Variant((uint64_t) p_item); } };
9+
template<typename T> struct TFormat { static Variant from(const T& p_item) { return p_item; } };
10+
template<> struct TFormat<IndexSafe64> { static Variant from(const IndexSafe64& p_item) { return *p_item; } };
11+
template<> struct TFormat<Index64> { static Variant from(const Index64& p_item) { return *p_item; } };
12+
template<> struct TFormat<Index32> { static Variant from(const Index32& p_item) { return *p_item; } };
13+
template<> struct TFormat<uintptr_t> { static Variant from(const uintptr_t& p_item) { return Variant((uint64_t) p_item); } };
1314
template<typename T> static Variant convert(const T& p_item) { return TFormat<T>::from(p_item); }
1415

1516
template <typename... VarArgs>

internal/jsb_sindex.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,12 @@ namespace jsb::internal
7575
UnderlyingType packed_;
7676
};
7777

78+
// Allocates 64 bits, but only the lower 52 are used. This is the range that can be safely stored in a JS number.
79+
// index(0, 4_294_967_295) revision(1, 4_294_967_295)
80+
typedef TIndex<uint64_t, 20, 0xfffff> IndexSafe64;
81+
7882
// do not really support the index bigger than int32_t.MaxValue
79-
// /*if unsigned*/ index(0, 4_294_967_295) revision(1, 4_294_967_295)
83+
// /*if unsigned*/ index(0, 4_294_967_295) revision(1, 1_048_575)
8084
typedef TIndex<uint64_t, 32, 0xffffffff> Index64;
8185

8286
// index(0, 67_108_863) revision(1, 63)

internal/jsb_timer_manager.h

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,23 @@ namespace jsb::internal
1010
jsb_force_inline TimerHandle() = default;
1111
jsb_force_inline ~TimerHandle() = default;
1212

13-
jsb_force_inline TimerHandle(const Index32& p_index): id(p_index) {}
14-
jsb_force_inline TimerHandle& operator=(const Index32& p_index) { id = p_index; return *this; }
13+
jsb_force_inline TimerHandle(const IndexSafe64& p_index): id(p_index) {}
14+
jsb_force_inline TimerHandle& operator=(const IndexSafe64& p_index) { id = p_index; return *this; }
1515

1616
jsb_force_inline TimerHandle(const TimerHandle& p_other): id(p_other.id) {}
1717
jsb_force_inline TimerHandle& operator=(const TimerHandle& p_other) { id = p_other.id; return *this; }
1818

1919
jsb_force_inline TimerHandle(TimerHandle&& p_other) noexcept: id(p_other.id) {}
2020
jsb_force_inline TimerHandle& operator=(TimerHandle&& p_other) noexcept { id = p_other.id; p_other.id = {}; return *this; }
2121

22-
jsb_force_inline explicit operator int32_t() const { return (int32_t) *id; }
23-
jsb_force_inline explicit operator Index32() const { return id; }
24-
jsb_force_inline explicit operator bool() const { return id != Index32::none(); }
22+
jsb_force_inline explicit operator int64_t() const { return (int64_t) *id; }
23+
jsb_force_inline explicit operator IndexSafe64() const { return id; }
24+
jsb_force_inline explicit operator bool() const { return id != IndexSafe64::none(); }
2525

26-
jsb_force_inline explicit TimerHandle(const int32_t p_index): id((uint32_t) p_index) {}
26+
jsb_force_inline explicit TimerHandle(const int64_t p_index): id((int64_t) p_index) {}
2727

2828
private:
29-
Index32 id;
29+
IndexSafe64 id;
3030

3131
template<typename, uint8_t, uint8_t, uint64_t>
3232
friend class TTimerManager;
@@ -59,11 +59,11 @@ namespace jsb::internal
5959

6060
struct WheelSlot
6161
{
62-
std::vector<Index32> timer_indices;
62+
std::vector<IndexSafe64> timer_indices;
6363

64-
jsb_force_inline void append(const Index32& index) { timer_indices.push_back(index); }
64+
jsb_force_inline void append(const IndexSafe64& index) { timer_indices.push_back(index); }
6565

66-
jsb_force_inline void move_to(std::vector<Index32>& cache)
66+
jsb_force_inline void move_to(std::vector<IndexSafe64>& cache)
6767
{
6868
cache.reserve(cache.size() + timer_indices.size());
6969
cache.insert(cache.end(), timer_indices.begin(), timer_indices.end());
@@ -97,15 +97,15 @@ namespace jsb::internal
9797
index = 0;
9898
}
9999

100-
uint32_t add(uint64_t p_delay, const Index32& p_timer_index)
100+
uint64_t add(uint64_t p_delay, const IndexSafe64& p_timer_index)
101101
{
102102
const uint64_t offset = p_delay >= interval ? (p_delay / interval) - 1 : p_delay / interval;
103-
const uint32_t slot_index = (uint32_t)((index + offset) % (uint64_t)kWheelSlotNum);
103+
const uint64_t slot_index = (uint64_t)((index + offset) % (uint64_t)kWheelSlotNum);
104104
slots[slot_index].append(p_timer_index);
105105
return slot_index;
106106
}
107107

108-
void next(std::vector<Index32>& p_active_indices)
108+
void next(std::vector<IndexSafe64>& p_active_indices)
109109
{
110110
slots[index++].move_to(p_active_indices);
111111
}
@@ -132,10 +132,10 @@ namespace jsb::internal
132132

133133
uint64_t _elapsed;
134134
uint32_t _time_slice;
135-
SArray<TimerData, Index32> _used_timers;
135+
SArray<TimerData, IndexSafe64> _used_timers;
136136
WheelState _wheels[kWheelNum];
137-
std::vector<Index32> _activated_timers;
138-
std::vector<Index32> _moving_timers;
137+
std::vector<IndexSafe64> _activated_timers;
138+
std::vector<IndexSafe64> _moving_timers;
139139

140140
static void check_internal_state()
141141
{
@@ -184,7 +184,7 @@ namespace jsb::internal
184184
_used_timers.remove_at(inout_handle.id);
185185

186186
const uint64_t delay = p_first_delay > 0 ? p_first_delay : p_rate;
187-
const Index32 index = _used_timers.add(TimerData());
187+
const IndexSafe64 index = _used_timers.add(TimerData());
188188
TimerData& timer = _used_timers.get_value(index);
189189
timer.rate = p_rate;
190190
timer.expires = delay + _elapsed;
@@ -206,7 +206,7 @@ namespace jsb::internal
206206
{
207207
if (_clear_timer(p_handle.id))
208208
{
209-
p_handle.id = Index32::none();
209+
p_handle.id = IndexSafe64::none();
210210
return true;
211211
}
212212
return false;
@@ -258,7 +258,7 @@ namespace jsb::internal
258258
}
259259

260260
_wheels[wheel_index + 1].next(_moving_timers);
261-
for (const Index32& index : _moving_timers)
261+
for (const IndexSafe64& index : _moving_timers)
262262
{
263263
TimerData* timer;
264264
if (_used_timers.try_get_value_pointer(index, timer))
@@ -284,7 +284,7 @@ namespace jsb::internal
284284
bool invoke_timers(TContext* ctx)
285285
{
286286
if (_activated_timers.empty()) return false;
287-
for (const Index32& index : _activated_timers)
287+
for (const IndexSafe64& index : _activated_timers)
288288
{
289289
TimerData* timer;
290290
if (!_used_timers.try_get_value_pointer(index, timer))
@@ -314,13 +314,13 @@ namespace jsb::internal
314314
}
315315

316316
private:
317-
bool _clear_timer(const Index32& p_index)
317+
bool _clear_timer(const IndexSafe64& p_index)
318318
{
319319
check_internal_state();
320320
return _used_timers.remove_at(p_index);
321321
}
322322

323-
void rearrange_timer(const Index32& p_timer_id, uint64_t p_delay)
323+
void rearrange_timer(const IndexSafe64& p_timer_id, uint64_t p_delay)
324324
{
325325
jsb_check(_used_timers.is_valid_index(p_timer_id));
326326
for (WheelState& wheel : _wheels)

0 commit comments

Comments
 (0)