Skip to content

Commit e9afcdf

Browse files
authored
Fix camelCase convertion and remove List.fold_left in style_tag (#556)
* Remove List.fold_left in get_string_style_hashes * Remove String.make on camelCaseToKebabCase * Push benchmark for CSS * Use List.iteri instead of ref first * Remove dep on benchmark * Add random message in benchmark main * Use ubuntu-latest * Update version ocaml/setup-ocaml * Push more ubuntu-latest in CI
1 parent 52f3f05 commit e9afcdf

File tree

5 files changed

+205
-23
lines changed

5 files changed

+205
-23
lines changed

.github/workflows/ci.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525
name: Build
2626
strategy:
2727
matrix:
28-
os: [ubuntu-20.04, macos-latest] # Missing windows-latest
28+
os: [ubuntu-latest, macos-latest] # windows-latest
2929
ocaml-compiler:
3030
- 4.14.1
3131
- 5.2.0
@@ -50,7 +50,7 @@ jobs:
5050
key: opam-${{ matrix.os }}-${{ matrix.ocaml-compiler }}-${{ hashFiles('**.opam') }}
5151

5252
- name: Use OCaml ${{ matrix.ocaml-compiler }}
53-
uses: ocaml/setup-ocaml@v3.0.15
53+
uses: ocaml/setup-ocaml@v3.3.2
5454
if: steps.opam-cache.outputs.cache-hit != 'true'
5555
with:
5656
ocaml-compiler: ${{ matrix.ocaml-compiler }}
@@ -109,7 +109,7 @@ jobs:
109109
path: _build/default/packages/bin/bin.exe
110110

111111
- name: Upload runtime artifacts for @davesnx/styled-ppx/runtime
112-
if: matrix.os == 'ubuntu-20.04' && matrix.ocaml-compiler == '4.14.1'
112+
if: matrix.os == 'ubuntu-latest' && matrix.ocaml-compiler == '4.14.1'
113113
uses: actions/upload-artifact@v4
114114
with:
115115
name: runtime-rescript
@@ -139,7 +139,7 @@ jobs:
139139
name: Publish
140140
needs: build
141141
if: github.repository_owner == 'davesnx'
142-
runs-on: ubuntu-20.04
142+
runs-on: ubuntu-latest
143143

144144
steps:
145145
- uses: actions/checkout@v4
@@ -157,7 +157,7 @@ jobs:
157157
- name: Download linux artifacts
158158
uses: actions/download-artifact@v4
159159
with:
160-
name: ubuntu-20.04
160+
name: ubuntu-latest
161161
path: _release/platform-linux-x64
162162

163163
- name: Download macOS artifacts

packages/runtime/benchmark/dune

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
(executable
2+
(name main))

packages/runtime/benchmark/main.ml

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
(* Simple Performance Benchmark, not related with the CSS runtime, just used for manually testing some internal functions *)
2+
3+
let time_function name f =
4+
let start_time = Sys.time () in
5+
let result = f () in
6+
let end_time = Sys.time () in
7+
Printf.printf "%s: %.4f seconds\n" name (end_time -. start_time);
8+
result
9+
10+
let memory_stats name f =
11+
Gc.compact ();
12+
let stat_before = Gc.stat () in
13+
let result = f () in
14+
Gc.compact ();
15+
let stat_after = Gc.stat () in
16+
17+
let allocations =
18+
stat_after.minor_words
19+
+. stat_after.major_words
20+
-. stat_before.minor_words
21+
-. stat_before.major_words
22+
in
23+
24+
Printf.printf "%s: %.0f words allocated (%.2f MB)\n" name allocations
25+
(allocations *. 8.0 /. 1024.0 /. 1024.0);
26+
result
27+
28+
let generate_hashes n =
29+
let rec loop acc i =
30+
if i <= 0 then acc
31+
else loop (Printf.sprintf "css-hash-%d-abcdef" i :: acc) (i - 1)
32+
in
33+
loop [] n
34+
35+
let test_camel_strings =
36+
[|
37+
"backgroundColor";
38+
"borderTopLeftRadius";
39+
"marginBottomRight";
40+
"paddingTopBottomLeftRight";
41+
"textTransformUppercase";
42+
"fontWeightBoldItalic";
43+
"boxShadowInsetOutset";
44+
"transformOriginCenter";
45+
"animationDurationTimingFunction";
46+
"borderImageOutsetSliceWidthSource";
47+
|]
48+
49+
(* OLD IMPLEMENTATIONS *)
50+
let explode s =
51+
let rec explode_rec acc i =
52+
if i < 0 then acc else explode_rec (s.[i] :: acc) (i - 1)
53+
in
54+
explode_rec [] (String.length s - 1)
55+
56+
let old_camelCaseToKebabCase str =
57+
let insert_dash acc letter =
58+
match letter with
59+
| 'A' .. 'Z' as letter ->
60+
("-" ^ String.make 1 (Char.lowercase_ascii letter)) :: acc
61+
| _ -> String.make 1 letter :: acc
62+
in
63+
String.concat "" (List.rev (List.fold_left insert_dash [] (explode str)))
64+
65+
let old_get_string_style_hashes hashes =
66+
List.fold_left
67+
(fun accumulator hash ->
68+
String.trim @@ Printf.sprintf "%s %s" accumulator hash)
69+
"" hashes
70+
71+
(* NEW IMPLEMENTATIONS *)
72+
let new_camelCaseToKebabCase str =
73+
let len = String.length str in
74+
let buffer = Buffer.create (len + 10) in
75+
for i = 0 to len - 1 do
76+
let c = str.[i] in
77+
match c with
78+
| 'A' .. 'Z' ->
79+
if i > 0 then Buffer.add_char buffer '-';
80+
Buffer.add_char buffer (Char.lowercase_ascii c)
81+
| _ -> Buffer.add_char buffer c
82+
done;
83+
Buffer.contents buffer
84+
85+
let new_get_string_style_hashes hashes =
86+
let buffer = Buffer.create 1024 in
87+
let first = ref true in
88+
List.iter
89+
(fun hash ->
90+
if not !first then Buffer.add_char buffer ' ';
91+
Buffer.add_string buffer (String.trim hash);
92+
first := false)
93+
hashes;
94+
Buffer.contents buffer
95+
96+
(* BENCHMARKS *)
97+
let benchmark_camelcase n =
98+
Printf.printf "\n=== CamelCase Conversion (%d iterations) ===\n" n;
99+
let test_strings = Array.to_list (Array.make n test_camel_strings.(0)) in
100+
101+
let old_result =
102+
memory_stats "OLD camelCase" (fun () ->
103+
time_function "OLD time" (fun () ->
104+
List.map old_camelCaseToKebabCase test_strings))
105+
in
106+
107+
let new_result =
108+
memory_stats "NEW camelCase" (fun () ->
109+
time_function "NEW time" (fun () ->
110+
List.map new_camelCaseToKebabCase test_strings))
111+
in
112+
113+
Printf.printf "Results match: %b\n"
114+
(List.for_all2 String.equal old_result new_result)
115+
116+
let benchmark_hash_concat n =
117+
Printf.printf "\n=== Hash Concatenation (%d hashes) ===\n" n;
118+
let test_hashes = generate_hashes n in
119+
120+
let old_result =
121+
memory_stats "OLD hash concat" (fun () ->
122+
time_function "OLD time" (fun () ->
123+
old_get_string_style_hashes test_hashes))
124+
in
125+
126+
let new_result =
127+
memory_stats "NEW hash concat" (fun () ->
128+
time_function "NEW time" (fun () ->
129+
new_get_string_style_hashes test_hashes))
130+
in
131+
132+
Printf.printf "Results match: %b\n" (String.equal old_result new_result);
133+
Printf.printf "Result length: %d chars\n" (String.length old_result)
134+
135+
let stress_test () =
136+
Printf.printf "\n=== STRESS TEST ===\n";
137+
let large_hashes = generate_hashes 5000 in
138+
let large_camel_list =
139+
Array.to_list (Array.make 2000 "veryLongCamelCasePropertyName")
140+
in
141+
142+
Printf.printf "Testing with 5000 hashes + 2000 camelCase conversions...\n";
143+
144+
let _ =
145+
memory_stats "OLD stress test" (fun () ->
146+
time_function "OLD total time" (fun () ->
147+
let hash_result = old_get_string_style_hashes large_hashes in
148+
let camel_results =
149+
List.map old_camelCaseToKebabCase large_camel_list
150+
in
151+
hash_result, camel_results))
152+
in
153+
154+
let _ =
155+
memory_stats "NEW stress test" (fun () ->
156+
time_function "NEW total time" (fun () ->
157+
let hash_result = new_get_string_style_hashes large_hashes in
158+
let camel_results =
159+
List.map new_camelCaseToKebabCase large_camel_list
160+
in
161+
hash_result, camel_results))
162+
in
163+
164+
Printf.printf "Stress test completed\n"
165+
166+
let () =
167+
Printf.printf "=== CSS Performance Benchmark ===\n";
168+
169+
benchmark_camelcase 100;
170+
benchmark_camelcase 1000;
171+
172+
benchmark_hash_concat 100;
173+
benchmark_hash_concat 1000;
174+
benchmark_hash_concat 5000;
175+
176+
stress_test ()

packages/runtime/native/CSS.ml

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -488,11 +488,14 @@ let get_stylesheet () =
488488
Buffer.contents buffer
489489

490490
let get_string_style_hashes () =
491-
Stylesheet.get_all instance
492-
|> List.fold_left
493-
(fun accumulator (hash, _) ->
494-
String.trim @@ Printf.sprintf "%s %s" accumulator hash)
495-
""
491+
let stylesheet = Stylesheet.get_all instance in
492+
let initial_size = List.length stylesheet * approximate_chars_in_rules in
493+
let buffer = Buffer.create initial_size in
494+
stylesheet
495+
|> List.iteri (fun i (hash, _) ->
496+
if i > 0 then Buffer.add_char buffer ' ';
497+
Buffer.add_string buffer (String.trim hash));
498+
Buffer.contents buffer
496499

497500
let style_tag ?key:_ ?children:_ () =
498501
React.createElement "style"

packages/runtime/native/Rule.ml

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,22 @@ type rule =
22
| Declaration of string * string
33
| Selector of string array * rule array
44

5-
let explode s =
6-
let rec exp i l = if i < 0 then l else exp (i - 1) (s.[i] :: l) in
7-
exp (String.length s - 1) []
8-
9-
let camelCaseToKebabCase str =
10-
let insert_dash acc letter =
11-
match letter with
12-
| 'A' .. 'Z' as letter ->
13-
("-" ^ String.make 1 (Char.lowercase_ascii letter)) :: acc
14-
| _ -> String.make 1 letter :: acc
15-
in
16-
String.concat "" (List.rev (List.fold_left insert_dash [] (explode str)))
5+
let camel_case_to_kebab_case str =
6+
let len = String.length str in
7+
let extra_space_for_dashes = 10 in
8+
let buffer = Buffer.create (len + extra_space_for_dashes) in
9+
for i = 0 to len - 1 do
10+
let c = str.[i] in
11+
match c with
12+
| 'A' .. 'Z' ->
13+
Buffer.add_char buffer '-';
14+
Buffer.add_char buffer (Char.lowercase_ascii c)
15+
| _ -> Buffer.add_char buffer c
16+
done;
17+
Buffer.contents buffer
1718

1819
let declaration (property, value) =
19-
Declaration (camelCaseToKebabCase property, value)
20+
Declaration (camel_case_to_kebab_case property, value)
2021

2122
let selector selector rules = Selector ([| selector |], rules)
2223
let selectorMany selector_list rules = Selector (selector_list, rules)

0 commit comments

Comments
 (0)