Skip to content

Commit 1bd88e8

Browse files
committed
implement RenderWork based send async initiation
1 parent 9f200f3 commit 1bd88e8

7 files changed

Lines changed: 179 additions & 87 deletions

File tree

tsunami/app/atom.go

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -117,13 +117,8 @@ func (a Atom[T]) SetFn(fn func(T) T) {
117117
logInvalidAtomSet(a.name)
118118
return
119119
}
120-
err := a.client.Root.SetFnAtomVal(a.name, func(val any) any {
121-
typedVal := util.GetTypedAtomValue[T](val, a.name)
122-
return fn(typedVal)
123-
})
124-
if err != nil {
125-
log.Printf("Failed to set atom value for %s: %v", a.name, err)
126-
return
127-
}
128-
a.client.Root.AtomAddRenderWork(a.name)
120+
currentVal := a.Get()
121+
copiedVal := DeepCopy(currentVal)
122+
newVal := fn(copiedVal)
123+
a.Set(newVal)
129124
}

tsunami/demo/cpuchart/app.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,6 @@ var App = app.DefineComponent("App", func(_ struct{}) any {
172172
}
173173
return data
174174
})
175-
app.SendAsyncInitiation()
176175
}, []any{})
177176

178177
handleClear := func() {

tsunami/demo/cpuchart/static/tw.css

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -252,33 +252,12 @@
252252
.sticky {
253253
position: sticky;
254254
}
255-
.-inset-1 {
256-
inset: calc(var(--spacing) * -1);
257-
}
258255
.isolate {
259256
isolation: isolate;
260257
}
261258
.isolation-auto {
262259
isolation: auto;
263260
}
264-
.\!container {
265-
width: 100% !important;
266-
@media (width >= 40rem) {
267-
max-width: 40rem !important;
268-
}
269-
@media (width >= 48rem) {
270-
max-width: 48rem !important;
271-
}
272-
@media (width >= 64rem) {
273-
max-width: 64rem !important;
274-
}
275-
@media (width >= 80rem) {
276-
max-width: 80rem !important;
277-
}
278-
@media (width >= 96rem) {
279-
max-width: 96rem !important;
280-
}
281-
}
282261
.container {
283262
width: 100%;
284263
@media (width >= 40rem) {
@@ -875,18 +854,10 @@
875854
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
876855
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
877856
}
878-
.ring {
879-
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
880-
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
881-
}
882857
.inset-ring {
883858
--tw-inset-ring-shadow: inset 0 0 0 1px var(--tw-inset-ring-color, currentcolor);
884859
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
885860
}
886-
.outline {
887-
outline-style: var(--tw-outline-style);
888-
outline-width: 1px;
889-
}
890861
.blur {
891862
--tw-blur: blur(8px);
892863
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
@@ -896,18 +867,10 @@
896867
--tw-drop-shadow: drop-shadow(0 1px 2px rgb(0 0 0 / 0.1)) drop-shadow( 0 1px 1px rgb(0 0 0 / 0.06));
897868
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
898869
}
899-
.grayscale {
900-
--tw-grayscale: grayscale(100%);
901-
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
902-
}
903870
.invert {
904871
--tw-invert: invert(100%);
905872
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
906873
}
907-
.sepia {
908-
--tw-sepia: sepia(100%);
909-
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
910-
}
911874
.filter {
912875
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
913876
}
@@ -935,11 +898,6 @@
935898
-webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
936899
backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
937900
}
938-
.transition {
939-
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events;
940-
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
941-
transition-duration: var(--tw-duration, var(--default-transition-duration));
942-
}
943901
.transition-colors {
944902
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;
945903
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
@@ -1145,11 +1103,6 @@
11451103
inherits: false;
11461104
initial-value: 0 0 #0000;
11471105
}
1148-
@property --tw-outline-style {
1149-
syntax: "*";
1150-
inherits: false;
1151-
initial-value: solid;
1152-
}
11531106
@property --tw-blur {
11541107
syntax: "*";
11551108
inherits: false;
@@ -1283,7 +1236,6 @@
12831236
--tw-ring-offset-width: 0px;
12841237
--tw-ring-offset-color: #fff;
12851238
--tw-ring-offset-shadow: 0 0 #0000;
1286-
--tw-outline-style: solid;
12871239
--tw-blur: initial;
12881240
--tw-brightness: initial;
12891241
--tw-contrast: initial;

tsunami/engine/asyncnotify.go

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
// Copyright 2025, Command Line Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package engine
5+
6+
import (
7+
"log"
8+
"time"
9+
)
10+
11+
const NotifyMaxCadence = 10 * time.Millisecond
12+
const NotifyDebounceTime = 500 * time.Microsecond
13+
const NotifyMaxDebounceTime = 2 * time.Millisecond
14+
15+
func (c *ClientImpl) notifyAsyncRenderWork() {
16+
log.Printf("notify async work\n")
17+
c.notifyOnce.Do(func() {
18+
c.notifyWakeCh = make(chan struct{}, 1)
19+
go c.asyncInitiationLoop()
20+
})
21+
22+
nowNs := time.Now().UnixNano()
23+
c.notifyLastEventNs.Store(nowNs)
24+
// Establish batch start if there's no active batch.
25+
if c.notifyBatchStartNs.Load() == 0 {
26+
c.notifyBatchStartNs.CompareAndSwap(0, nowNs)
27+
}
28+
// Coalesced wake-up.
29+
select {
30+
case c.notifyWakeCh <- struct{}{}:
31+
default:
32+
}
33+
}
34+
35+
func (c *ClientImpl) asyncInitiationLoop() {
36+
var (
37+
lastSent time.Time
38+
timer *time.Timer
39+
timerC <-chan time.Time
40+
)
41+
42+
schedule := func() {
43+
firstNs := c.notifyBatchStartNs.Load()
44+
if firstNs == 0 {
45+
// No pending batch; stop timer if running.
46+
if timer != nil {
47+
if !timer.Stop() {
48+
select {
49+
case <-timer.C:
50+
default:
51+
}
52+
}
53+
}
54+
timerC = nil
55+
return
56+
}
57+
lastNs := c.notifyLastEventNs.Load()
58+
59+
first := time.Unix(0, firstNs)
60+
last := time.Unix(0, lastNs)
61+
cadenceReady := lastSent.Add(NotifyMaxCadence)
62+
63+
// Reset the 2ms "max debounce" window at the cadence boundary:
64+
// deadline = max(first, cadenceReady) + 2ms
65+
anchor := first
66+
if cadenceReady.After(anchor) {
67+
anchor = cadenceReady
68+
}
69+
deadline := anchor.Add(NotifyMaxDebounceTime)
70+
71+
// candidate = min(last+500us, deadline)
72+
candidate := last.Add(NotifyDebounceTime)
73+
if deadline.Before(candidate) {
74+
candidate = deadline
75+
}
76+
77+
// final target = max(cadenceReady, candidate)
78+
target := candidate
79+
if cadenceReady.After(target) {
80+
target = cadenceReady
81+
}
82+
83+
d := time.Until(target)
84+
if d < 0 {
85+
d = 0
86+
}
87+
if timer == nil {
88+
timer = time.NewTimer(d)
89+
} else {
90+
if !timer.Stop() {
91+
select {
92+
case <-timer.C:
93+
default:
94+
}
95+
}
96+
timer.Reset(d)
97+
}
98+
timerC = timer.C
99+
}
100+
101+
for {
102+
select {
103+
case <-c.notifyWakeCh:
104+
schedule()
105+
106+
case <-timerC:
107+
now := time.Now()
108+
109+
// Recompute right before sending; if a late event arrived,
110+
// push the fire time out to respect the debounce.
111+
firstNs := c.notifyBatchStartNs.Load()
112+
if firstNs == 0 {
113+
// Nothing to do.
114+
continue
115+
}
116+
lastNs := c.notifyLastEventNs.Load()
117+
118+
first := time.Unix(0, firstNs)
119+
last := time.Unix(0, lastNs)
120+
cadenceReady := lastSent.Add(NotifyMaxCadence)
121+
122+
anchor := first
123+
if cadenceReady.After(anchor) {
124+
anchor = cadenceReady
125+
}
126+
deadline := anchor.Add(NotifyMaxDebounceTime)
127+
128+
candidate := last.Add(NotifyDebounceTime)
129+
if deadline.Before(candidate) {
130+
candidate = deadline
131+
}
132+
target := candidate
133+
if cadenceReady.After(target) {
134+
target = cadenceReady
135+
}
136+
137+
// If we're early (because a new event just came in), reschedule.
138+
if now.Before(target) {
139+
d := time.Until(target)
140+
if d < 0 {
141+
d = 0
142+
}
143+
if !timer.Stop() {
144+
select {
145+
case <-timer.C:
146+
default:
147+
}
148+
}
149+
timer.Reset(d)
150+
continue
151+
}
152+
153+
// Fire.
154+
_ = c.SendAsyncInitiation()
155+
lastSent = now
156+
157+
// Close current batch; a concurrent notify will CAS a new start.
158+
c.notifyBatchStartNs.Store(0)
159+
160+
// If anything is already pending, this will arm the next timer.
161+
schedule()
162+
}
163+
}
164+
}

tsunami/engine/atomimpl.go

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,6 @@ func (a *AtomImpl[T]) SetVal(val any) error {
6464
return a.setVal_nolock(val)
6565
}
6666

67-
func (a *AtomImpl[T]) SetFnVal(setFn func(any) any) error {
68-
a.lock.Lock()
69-
defer a.lock.Unlock()
70-
71-
newVal := setFn(a.val)
72-
return a.setVal_nolock(newVal)
73-
}
74-
7567
func (a *AtomImpl[T]) SetUsedBy(waveId string, used bool) {
7668
a.lock.Lock()
7769
defer a.lock.Unlock()

tsunami/engine/clientimpl.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"os"
1414
"strings"
1515
"sync"
16+
"sync/atomic"
1617
"time"
1718
"unicode"
1819

@@ -26,10 +27,6 @@ const TsunamiListenAddrEnvVar = "TSUNAMI_LISTENADDR"
2627
const DefaultListenAddr = "localhost:0"
2728
const DefaultComponentName = "App"
2829

29-
const NotifyMaxCadence = 10 * time.Millisecond
30-
const NotifyDebounceTime = 500 * time.Microsecond
31-
const NotifyMaxDebounceTime = 2 * time.Millisecond
32-
3330
type ssEvent struct {
3431
Event string
3532
Data []byte
@@ -53,6 +50,14 @@ type ClientImpl struct {
5350
AssetsFS fs.FS
5451
StaticFS fs.FS
5552
ManifestFileBytes []byte
53+
54+
// for notification
55+
// Atomics so we never drop "last event" timing info even if wakeCh is full.
56+
// 0 means "no pending batch".
57+
notifyOnce sync.Once
58+
notifyWakeCh chan struct{}
59+
notifyBatchStartNs atomic.Int64 // ns of first event in current batch
60+
notifyLastEventNs atomic.Int64 // ns of most recent event
5661
}
5762

5863
func makeClient() *ClientImpl {
@@ -210,6 +215,7 @@ func (c *ClientImpl) listenAndServe(ctx context.Context) error {
210215
}
211216

212217
func (c *ClientImpl) SendAsyncInitiation() error {
218+
log.Printf("send async initiation\n")
213219
if c.GetIsDone() {
214220
return fmt.Errorf("client is done")
215221
}
@@ -222,10 +228,6 @@ func (c *ClientImpl) SendAsyncInitiation() error {
222228
}
223229
}
224230

225-
func (c *ClientImpl) notifyAsyncRenderWork() {
226-
// TODO
227-
}
228-
229231
func makeNullRendered() *rpctypes.RenderedElem {
230232
return &rpctypes.RenderedElem{WaveId: uuid.New().String(), Tag: vdom.WaveNullTag}
231233
}

tsunami/engine/rootelem.go

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ type EffectWorkElem struct {
2727
type genAtom interface {
2828
GetVal() any
2929
SetVal(any) error
30-
SetFnVal(func(any) any) error
3130
SetUsedBy(string, bool)
3231
GetUsedBy() []string
3332
}
@@ -53,7 +52,7 @@ func (r *RootElem) addRenderWork(id string) {
5352
r.Client.notifyAsyncRenderWork()
5453
}
5554
}()
56-
55+
5756
r.needsRenderLock.Lock()
5857
defer r.needsRenderLock.Unlock()
5958

@@ -192,17 +191,6 @@ func (r *RootElem) SetAtomVal(name string, val any) error {
192191
return atom.SetVal(val)
193192
}
194193

195-
func (r *RootElem) SetFnAtomVal(name string, setFn func(any) any) error {
196-
r.atomLock.Lock()
197-
defer r.atomLock.Unlock()
198-
199-
atom, ok := r.Atoms[name]
200-
if !ok {
201-
return fmt.Errorf("atom %q not found", name)
202-
}
203-
return atom.SetFnVal(setFn)
204-
}
205-
206194
func (r *RootElem) RemoveAtom(name string) {
207195
r.atomLock.Lock()
208196
defer r.atomLock.Unlock()

0 commit comments

Comments
 (0)