Skip to content

Commit a489f8f

Browse files
committed
perf(nim): CoW container and model diff utils proposal
1 parent 15aefec commit a489f8f

File tree

68 files changed

+7159
-251
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

68 files changed

+7159
-251
lines changed

src/app/core/cow_seq.nim

Lines changed: 322 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
1+
## Copy-on-Write Sequence Container
2+
##
3+
## Provides a seq-like container with transparent Copy-on-Write semantics.
4+
## Memory is shared until a mutation occurs, at which point a copy is made.
5+
##
6+
## Key features:
7+
## - Transparent CoW via =copy hook
8+
## - seq-compatible API
9+
## - O(1) copy operations
10+
## - O(n) mutation operations (only when shared)
11+
## - Value type semantics (can be copied, no GC pressure)
12+
##
13+
## Example:
14+
## ```nim
15+
## var original = @[1, 2, 3].toCowSeq()
16+
## var copy = original # O(1) - shares memory
17+
## copy.add(4) # Copy-on-Write triggered
18+
## # original: [1, 2, 3]
19+
## # copy: [1, 2, 3, 4]
20+
## ```
21+
22+
import std/[hashes]
23+
24+
type
25+
CowSeqData*[T] = ref object
26+
## Internal data container with reference counting
27+
data: seq[T]
28+
refCount: int
29+
30+
CowSeq*[T] = object
31+
## Copy-on-Write sequence container
32+
## Behaves like seq[T] but shares memory until mutation
33+
dataRef: CowSeqData[T]
34+
35+
#
36+
# Constructors
37+
#
38+
39+
proc newCowSeq*[T](initialData: seq[T] = @[]): CowSeq[T] =
40+
## Create a new CowSeq from a regular seq
41+
## Time: O(1) - just wraps the seq
42+
result.dataRef = CowSeqData[T](data: initialData, refCount: 1)
43+
44+
proc newCowSeq*[T](size: int): CowSeq[T] =
45+
## Create a new CowSeq with pre-allocated size
46+
## Time: O(n)
47+
result.dataRef = CowSeqData[T](data: newSeq[T](size), refCount: 1)
48+
49+
proc toCowSeq*[T](s: seq[T]): CowSeq[T] {.inline.} =
50+
## Convert a regular seq to CowSeq
51+
## Time: O(1)
52+
newCowSeq(s)
53+
54+
#
55+
# Lifecycle hooks (transparent CoW!)
56+
#
57+
58+
proc `=copy`*[T](dest: var CowSeq[T], src: CowSeq[T]) =
59+
## Copy hook - implements transparent Copy-on-Write
60+
## This makes copies O(1) by sharing the reference
61+
if dest.dataRef == src.dataRef:
62+
return # Self-assignment
63+
64+
# Release old reference
65+
if not dest.dataRef.isNil:
66+
dest.dataRef.refCount.dec
67+
if dest.dataRef.refCount <= 0:
68+
dest.dataRef = nil
69+
70+
# Share new reference
71+
dest.dataRef = src.dataRef
72+
if not dest.dataRef.isNil:
73+
dest.dataRef.refCount.inc
74+
75+
proc `=destroy`*[T](x: var CowSeq[T]) =
76+
## Destructor - decrements reference count
77+
if not x.dataRef.isNil:
78+
x.dataRef.refCount.dec
79+
if x.dataRef.refCount <= 0:
80+
# Last reference, clean up
81+
x.dataRef = nil
82+
83+
proc `=sink`*[T](dest: var CowSeq[T], src: CowSeq[T]) =
84+
## Sink hook - transfers ownership without incrementing refcount
85+
if dest.dataRef == src.dataRef:
86+
return
87+
88+
if not dest.dataRef.isNil:
89+
dest.dataRef.refCount.dec
90+
if dest.dataRef.refCount <= 0:
91+
dest.dataRef = nil
92+
93+
dest.dataRef = src.dataRef
94+
95+
#
96+
# Internal: Copy-on-Write trigger
97+
#
98+
99+
proc ensureUnique[T](self: var CowSeq[T]) =
100+
## Ensure this CowSeq has exclusive ownership of its data
101+
## Triggers Copy-on-Write if data is shared
102+
## Time: O(1) if not shared, O(n) if shared
103+
if self.dataRef.isNil:
104+
self.dataRef = CowSeqData[T](data: @[], refCount: 1)
105+
elif self.dataRef.refCount > 1:
106+
# Copy-on-Write happens here!
107+
let newData = self.dataRef.data # Deep copy of seq
108+
self.dataRef.refCount.dec
109+
self.dataRef = CowSeqData[T](data: newData, refCount: 1)
110+
111+
#
112+
# Read-only operations (O(1), no CoW)
113+
#
114+
115+
proc len*[T](self: CowSeq[T]): int {.inline.} =
116+
## Return the number of elements
117+
## Time: O(1)
118+
if self.dataRef.isNil: 0
119+
else: self.dataRef.data.len
120+
121+
proc high*[T](self: CowSeq[T]): int {.inline.} =
122+
## Return the highest valid index
123+
## Time: O(1)
124+
self.len - 1
125+
126+
proc low*[T](self: CowSeq[T]): int {.inline.} =
127+
## Return the lowest valid index (always 0)
128+
## Time: O(1)
129+
0
130+
131+
proc `[]`*[T](self: CowSeq[T], idx: int): lent T {.inline.} =
132+
## Access element at index (read-only)
133+
## Time: O(1)
134+
self.dataRef.data[idx]
135+
136+
proc `[]`*[T](self: CowSeq[T], slice: HSlice[int, int]): seq[T] =
137+
## Return a slice as a regular seq
138+
## Time: O(k) where k is slice size
139+
if self.dataRef.isNil:
140+
return @[]
141+
self.dataRef.data[slice]
142+
143+
#
144+
# Mutable operations (trigger CoW if shared)
145+
#
146+
147+
proc `[]=`*[T](self: var CowSeq[T], idx: int, val: T) =
148+
## Set element at index
149+
## Time: O(1) if not shared, O(n) if shared (CoW)
150+
self.ensureUnique()
151+
self.dataRef.data[idx] = val
152+
153+
proc add*[T](self: var CowSeq[T], val: T) =
154+
## Add element to end
155+
## Time: O(1) amortized if not shared, O(n) if shared (CoW)
156+
self.ensureUnique()
157+
self.dataRef.data.add(val)
158+
159+
proc add*[T](self: var CowSeq[T], other: CowSeq[T]) =
160+
## Add all elements from another CowSeq
161+
## Time: O(k) where k is other.len
162+
if other.len == 0:
163+
return
164+
self.ensureUnique()
165+
for item in other:
166+
self.dataRef.data.add(item)
167+
168+
proc add*[T](self: var CowSeq[T], other: seq[T]) =
169+
## Add all elements from a regular seq
170+
## Time: O(k) where k is other.len
171+
if other.len == 0:
172+
return
173+
self.ensureUnique()
174+
self.dataRef.data.add(other)
175+
176+
proc delete*[T](self: var CowSeq[T], idx: int) =
177+
## Delete element at index
178+
## Time: O(n)
179+
self.ensureUnique()
180+
self.dataRef.data.delete(idx)
181+
182+
proc delete*[T](self: var CowSeq[T], first: int, last: int) =
183+
## Delete range of elements [first..last]
184+
## Time: O(n)
185+
self.ensureUnique()
186+
# Nim's seq.delete doesn't have range, do it manually
187+
for i in countdown(last, first):
188+
self.dataRef.data.delete(i)
189+
190+
proc insert*[T](self: var CowSeq[T], val: T, idx: int = 0) =
191+
## Insert element at index
192+
## Time: O(n)
193+
self.ensureUnique()
194+
self.dataRef.data.insert(val, idx)
195+
196+
proc setLen*[T](self: var CowSeq[T], newLen: int) =
197+
## Set length (grows or shrinks)
198+
## Time: O(1) if shrinking, O(k) if growing
199+
self.ensureUnique()
200+
self.dataRef.data.setLen(newLen)
201+
202+
#
203+
# Iteration
204+
#
205+
206+
iterator items*[T](self: CowSeq[T]): lent T =
207+
## Iterate over elements (read-only)
208+
if not self.dataRef.isNil:
209+
for item in self.dataRef.data:
210+
yield item
211+
212+
iterator mitems*[T](self: var CowSeq[T]): var T =
213+
## Iterate over elements (mutable)
214+
## Triggers CoW if shared
215+
self.ensureUnique()
216+
for item in self.dataRef.data.mitems:
217+
yield item
218+
219+
iterator pairs*[T](self: CowSeq[T]): tuple[key: int, val: lent T] =
220+
## Iterate over (index, element) pairs
221+
if not self.dataRef.isNil:
222+
for i, item in self.dataRef.data.pairs:
223+
yield (i, item)
224+
225+
#
226+
# Conversion
227+
#
228+
229+
proc asSeq*[T](self: CowSeq[T]): seq[T] =
230+
## Convert to regular seq (creates a copy)
231+
## Named asSeq to avoid collision with sequtils.toSeq
232+
## Time: O(1) if just reading, O(n) if copying
233+
if self.dataRef.isNil: @[]
234+
else: self.dataRef.data
235+
236+
proc toOpenArray*[T](self: CowSeq[T], first, last: int): seq[T] =
237+
## Return a slice as a seq
238+
## Time: O(k) where k is slice size
239+
if self.dataRef.isNil or first > last or first < 0:
240+
return @[]
241+
242+
let lastIdx = min(last, self.high)
243+
result = newSeq[T](lastIdx - first + 1)
244+
for i in first..lastIdx:
245+
result[i - first] = self.dataRef.data[i]
246+
247+
#
248+
# Comparison
249+
#
250+
251+
proc `==`*[T](a, b: CowSeq[T]): bool =
252+
## Equality comparison
253+
## Time: O(1) if same reference, O(n) otherwise
254+
if a.dataRef == b.dataRef:
255+
return true # Same reference, definitely equal
256+
257+
if a.len != b.len:
258+
return false
259+
260+
for i in 0..<a.len:
261+
if a[i] != b[i]:
262+
return false
263+
264+
return true
265+
266+
proc hash*[T](self: CowSeq[T]): Hash =
267+
## Hash function for use in tables/sets
268+
## Time: O(n)
269+
var h: Hash = 0
270+
for item in self:
271+
h = h !& hash(item)
272+
result = !$h
273+
274+
#
275+
# Utility
276+
#
277+
278+
proc contains*[T](self: CowSeq[T], val: T): bool =
279+
## Check if value exists in sequence
280+
## Time: O(n)
281+
if self.dataRef.isNil:
282+
return false
283+
for item in self.dataRef.data:
284+
if item == val:
285+
return true
286+
return false
287+
288+
proc find*[T](self: CowSeq[T], val: T): int =
289+
## Find index of value, returns -1 if not found
290+
## Time: O(n)
291+
if self.dataRef.isNil:
292+
return -1
293+
for i, item in self.dataRef.data:
294+
if item == val:
295+
return i
296+
return -1
297+
298+
proc `$`*[T](self: CowSeq[T]): string =
299+
## String representation
300+
if self.dataRef.isNil:
301+
return "@[]"
302+
result = "@["
303+
for i, item in self:
304+
if i > 0:
305+
result.add(", ")
306+
result.add($item)
307+
result.add("]")
308+
309+
#
310+
# Debug helpers
311+
#
312+
313+
proc getRefCount*[T](self: CowSeq[T]): int =
314+
## Get current reference count (for debugging/testing)
315+
if self.dataRef.isNil: 0
316+
else: self.dataRef.refCount
317+
318+
proc isShared*[T](self: CowSeq[T]): bool =
319+
## Check if this CowSeq shares data with others
320+
if self.dataRef.isNil: false
321+
else: self.dataRef.refCount > 1
322+

src/app/modules/main/chat_section/controller.nim

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -722,18 +722,18 @@ proc allAccountsTokenBalance*(self: Controller, symbol: string): float64 =
722722
return self.walletAccountService.allAccountsTokenBalance(symbol)
723723

724724
proc getTokenDecimals*(self: Controller, symbol: string): int =
725-
let asset = self.tokenService.findTokenBySymbol(symbol)
726-
if asset != nil:
727-
return asset.decimals
725+
let assetOpt = self.tokenService.findTokenBySymbol(symbol)
726+
if assetOpt.isSome:
727+
return assetOpt.get().decimals
728728
return 0
729729

730730
# find addresses by tokenKey from UI
731731
# tokenKey can be: symbol for ERC20, or chain+address[+tokenId] for ERC721
732732
proc getContractAddressesForToken*(self: Controller, tokenKey: string): Table[int, string] =
733733
var contractAddresses = initTable[int, string]()
734-
let token = self.tokenService.findTokenBySymbol(tokenKey)
735-
if token != nil:
736-
for addrPerChain in token.addressPerChainId:
734+
let tokenOpt = self.tokenService.findTokenBySymbol(tokenKey)
735+
if tokenOpt.isSome:
736+
for addrPerChain in tokenOpt.get().addressPerChainId:
737737
# depending on areTestNetworksEnabled (in getNetworkByChainId), contractAddresses will
738738
# contain mainnets or testnets only
739739
let network = self.networkService.getNetworkByChainId(addrPerChain.chainId)

src/app/modules/main/communities/controller.nim

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import ./io_interface
33

44
import app/core/signals/types
55
import app/core/eventemitter
6+
import app/core/cow_seq
67
import app_service/service/chat/dto/chat
78
import app_service/service/community/service as community_service
89
import app_service/service/chat/service as chat_service
@@ -354,7 +355,7 @@ proc getAllCommunityTokens*(self: Controller): seq[CommunityTokenDto] =
354355
proc getNetworkByChainId*(self:Controller, chainId: int): NetworkItem =
355356
self.networksService.getNetworkByChainId(chainId)
356357

357-
proc getTokenBySymbolList*(self: Controller): seq[TokenBySymbolItem] =
358+
proc getTokenBySymbolList*(self: Controller): CowSeq[TokenBySymbolItem] =
358359
return self.tokenService.getTokenBySymbolList()
359360

360361
proc shareCommunityUrlWithChatKey*(self: Controller, communityId: string): string =

src/app/modules/main/communities/module.nim

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import ./models/discord_channels_model
1212
import ./models/discord_file_list_model
1313
import ./models/discord_import_task_item
1414
import ./models/discord_import_tasks_model
15+
import app/core/cow_seq
1516
import app/modules/shared_models/[section_model, section_item, token_permissions_model, token_permission_item,
1617
token_list_item, token_list_model, token_criteria_item, token_criteria_model, token_permission_chat_list_model, keypair_model]
1718
import app/global/global_singleton
@@ -575,7 +576,7 @@ proc buildTokensAndCollectiblesFromWallet(self: Module) =
575576

576577
# Common ERC20 tokens
577578
let allNetworks = self.controller.getCurrentNetworksChainIds()
578-
let erc20Tokens = self.controller.getTokenBySymbolList().filter(t => (block:
579+
let erc20Tokens = self.controller.getTokenBySymbolList().asSeq().filter(t => (block:
579580
let filteredChains = t.addressPerChainId.filter(apC => allNetworks.contains(apc.chainId))
580581
return filteredChains.len != 0
581582
))

src/app/modules/main/profile_section/profile/controller.nim

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import io_interface
22

33
import app/global/app_signals
44
import app/core/eventemitter
5+
import app/core/cow_seq
56
import app_service/service/profile/service as profile_service
67
import app_service/service/settings/service as settings_service
78
import app_service/service/community/service as community_service
@@ -97,5 +98,5 @@ proc getProfileShowcaseEntriesLimit*(self: Controller): int =
9798
proc requestCommunityInfo*(self: Controller, communityId: string, shard: Shard) =
9899
self.communityService.requestCommunityInfo(communityId, shard)
99100

100-
proc getTokenBySymbolList*(self: Controller): var seq[TokenBySymbolItem] =
101+
proc getTokenBySymbolList*(self: Controller): CowSeq[TokenBySymbolItem] =
101102
self.tokenService.getTokenBySymbolList()

0 commit comments

Comments
 (0)