Skip to content

Commit 825b030

Browse files
committed
xapi-stdext-std: Remove String.escaped, add String.replaced
String.replaced is an alias of map_unlikely. This names makes the intent of the function clearer. Because a function to replace the characters is exposed, users are less likely to fall into the pitfall of using lists. Lists not only are very slow, but allow users to have more than one replacement rule per character, possibly introducing mistakes. If a plain match function cannot be produced and a list needs to be used, users can convert it to a Char.Map and do the match with a find_opt. This approach ends up being ~60-70% faster than using plain lists. The benchmark comparing the new approach with the old one: String size 100: Optimized: 236.556 μs Reference: 1861.600 μs Improvement: 87.3% faster String size 500: Optimized: 1099.030 μs Reference: 9665.405 μs Improvement: 88.6% faster String size 1000: Optimized: 2198.777 μs Reference: 19115.019 μs Improvement: 88.5% faster Signed-off-by: Pau Ruiz Safont <pau.safont@vates.tech>
1 parent 899b771 commit 825b030

11 files changed

Lines changed: 93 additions & 137 deletions

File tree

ocaml/idl/markdown_backend.ml

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -43,41 +43,41 @@ let compare_case_ins x y =
4343
compare (String.lowercase_ascii x) (String.lowercase_ascii y)
4444

4545
let escape s =
46-
let esc_char = function
46+
let replace = function
4747
| '\\' ->
48-
"&#92;"
48+
Some "&#92;"
4949
| '*' ->
50-
"&#42;"
50+
Some "&#42;"
5151
| '_' ->
52-
"&#95;"
52+
Some "&#95;"
5353
| '{' ->
54-
"&#123;"
54+
Some "&#123;"
5555
| '}' ->
56-
"&#125;"
56+
Some "&#125;"
5757
| '[' ->
58-
"&#91;"
58+
Some "&#91;"
5959
| ']' ->
60-
"&#93;"
60+
Some "&#93;"
6161
| '(' ->
62-
"&#40;"
62+
Some "&#40;"
6363
| ')' ->
64-
"&#41;"
64+
Some "&#41;"
6565
| '>' ->
66-
"&gt;"
66+
Some "&gt;"
6767
| '<' ->
68-
"&lt;"
68+
Some "&lt;"
6969
| '#' ->
70-
"&#35;"
70+
Some "&#35;"
7171
| '+' ->
72-
"&#43;"
72+
Some "&#43;"
7373
| '-' ->
74-
"&#45;"
74+
Some "&#45;"
7575
| '!' ->
76-
"&#33;"
77-
| c ->
78-
String.make 1 c
76+
Some "&#33;"
77+
| _ ->
78+
None
7979
in
80-
String.to_seq s |> Seq.map esc_char |> List.of_seq |> String.concat ""
80+
Xapi_stdext_std.Xstringext.String.replaced ~replace s
8181

8282
let rec of_ty_verbatim = function
8383
| SecretString | String ->

ocaml/libs/http-lib/dune

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
xapi-backtrace
6464
xapi-log
6565
xapi-stdext-pervasives
66+
xapi-stdext-std
6667
xapi-stdext-threads
6768
xapi-stdext-unix))
6869

ocaml/libs/http-lib/http_svr.ml

Lines changed: 17 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -340,28 +340,22 @@ module Server = struct
340340
x.handlers []
341341
end
342342

343-
let escape str =
344-
(* from xapi-stdext-std xstringext *)
345-
let escaped ~rules string =
346-
let aux h t =
347-
( if List.mem_assoc h rules then
348-
List.assoc h rules
349-
else
350-
Astring.String.of_char h
343+
let escape_html str =
344+
Xapi_stdext_std.Xstringext.String.replaced
345+
~replace:(function
346+
| '<' ->
347+
Some "&lt;"
348+
| '>' ->
349+
Some "&gt;"
350+
| '\'' ->
351+
Some "&apos;"
352+
| '"' ->
353+
Some "&quot;"
354+
| '&' ->
355+
Some "&amp;"
356+
| _ ->
357+
None
351358
)
352-
:: t
353-
in
354-
String.concat "" (Astring.String.fold_right aux string [])
355-
in
356-
escaped
357-
~rules:
358-
[
359-
('<', "&lt;")
360-
; ('>', "&gt;")
361-
; ('\'', "&apos;")
362-
; ('"', "&quot;")
363-
; ('&', "&amp;")
364-
]
365359
str
366360

367361
exception Generic_error of string
@@ -518,7 +512,7 @@ let read_request ?proxy_seen ~read_timeout ~total_timeout ~max_length fd =
518512
)
519513
| exc ->
520514
response_internal_error exc fd
521-
~extra:(escape (Printexc.to_string exc))
515+
~extra:(escape_html (Printexc.to_string exc))
522516
) ;
523517
(None, None)
524518

@@ -567,7 +561,7 @@ let handle_one (x : 'a Server.t) ss context req =
567561
)
568562
| exc ->
569563
response_internal_error ~req exc ss
570-
~extra:(escape (Printexc.to_string exc))
564+
~extra:(escape_html (Printexc.to_string exc))
571565
) ;
572566
!finished
573567

ocaml/libs/http-lib/http_svr.mli

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ end
4949

5050
exception Generic_error of string
5151

52-
val escape : string -> string
53-
(** [escape str] escapes HTML/XML special characters in [str] for safe inclusion in HTML/XML content. *)
52+
val escape_html : string -> string
53+
(** [escape_html str] escapes HTML/XML special characters in [str] for safe inclusion in HTML/XML content. *)
5454

5555
type socket
5656

ocaml/libs/xapi-stdext/lib/xapi-stdext-std/bench/bench_xstringext.ml

Lines changed: 36 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,36 +8,42 @@ let make_string len = String.init len (fun i -> Char.chr (33 + (i mod 94)))
88
let escape_rules =
99
[('a', "[A]"); ('e', "[E]"); ('i', "[I]"); ('o', "[O]"); ('u', "[U]")]
1010

11-
(* Reference implementation from xstringext_test.ml *)
12-
let escaped_spec ?rules string =
13-
match rules with
14-
| None ->
15-
String.escaped string
16-
| Some rules ->
17-
let apply_rules char =
18-
match List.assoc_opt char rules with
19-
| None ->
20-
Seq.return char
21-
| Some replacement ->
22-
String.to_seq replacement
23-
in
24-
string |> String.to_seq |> Seq.concat_map apply_rules |> String.of_seq
11+
let replace = function
12+
| 'a' ->
13+
Some "[A]"
14+
| 'e' ->
15+
Some "[E]"
16+
| 'i' ->
17+
Some "[I]"
18+
| 'o' ->
19+
Some "[O]"
20+
| 'u' ->
21+
Some "[U]"
22+
| _ ->
23+
None
24+
25+
(* Reference implementation using lists *)
26+
let replaced_spec ~rules string =
27+
let apply_rules char = List.assoc_opt char rules in
28+
XString.replaced ~replace:apply_rules string
2529

26-
let escaped_benchmark n =
30+
let replaced ~rules string = XString.replaced ~replace:rules string
31+
32+
let replaced_benchmark n =
2733
let s = make_string n in
28-
Staged.stage @@ fun () -> ignore (XString.escaped ~rules:escape_rules s)
34+
Staged.stage @@ fun () -> ignore (replaced ~rules:replace s)
2935

30-
let escaped_spec_benchmark n =
36+
let replaced_spec_benchmark n =
3137
let s = make_string n in
32-
Staged.stage @@ fun () -> ignore (escaped_spec ~rules:escape_rules s)
38+
Staged.stage @@ fun () -> ignore (replaced_spec ~rules:escape_rules s)
3339

34-
let test_escaped =
35-
Test.make_indexed ~name:"escaped" ~fmt:"%s %d" ~args:[100; 500; 1000]
36-
escaped_benchmark
40+
let test_replaced =
41+
Test.make_indexed ~name:"replaced" ~fmt:"%s %d" ~args:[100; 500; 1000]
42+
replaced_benchmark
3743

38-
let test_escaped_spec =
39-
Test.make_indexed ~name:"escaped-spec" ~fmt:"%s %d" ~args:[100; 500; 1000]
40-
escaped_spec_benchmark
44+
let test_replaced_spec =
45+
Test.make_indexed ~name:"replaced-spec" ~fmt:"%s %d" ~args:[100; 500; 1000]
46+
replaced_spec_benchmark
4147

4248
let benchmark () =
4349
let ols =
@@ -50,8 +56,8 @@ let benchmark () =
5056
Benchmark.cfg ~limit:2000 ~quota:(Time.second 0.5) ~kde:(Some 1000) ()
5157
in
5258
let test =
53-
Test.make_grouped ~name:"escaped-comparison"
54-
[test_escaped; test_escaped_spec]
59+
Test.make_grouped ~name:"replaced-comparison"
60+
[test_replaced; test_replaced_spec]
5561
in
5662
let raw_results = Benchmark.all cfg instances test in
5763
let results =
@@ -97,8 +103,10 @@ let () =
97103
List.iter
98104
(fun size ->
99105
Printf.printf "String size %s:\n" size ;
100-
let opt_test = Printf.sprintf "escaped-comparison/escaped %s" size in
101-
let ref_test = Printf.sprintf "escaped-comparison/escaped-spec %s" size in
106+
let opt_test = Printf.sprintf "replaced-comparison/replaced %s" size in
107+
let ref_test =
108+
Printf.sprintf "replaced-comparison/replaced-spec %s" size
109+
in
102110
match (get_timing opt_test, get_timing ref_test) with
103111
| Some opt_time, Some ref_time ->
104112
let improvement = (ref_time -. opt_time) /. ref_time *. 100.0 in

ocaml/libs/xapi-stdext/lib/xapi-stdext-std/xstringext.ml

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -150,10 +150,5 @@ module String = struct
150150
) else
151151
s
152152

153-
let escaped ?rules s =
154-
match rules with
155-
| None ->
156-
String.escaped s
157-
| Some rules ->
158-
map_unlikely s (fun c -> List.assoc_opt c rules)
153+
let replaced ~replace s = map_unlikely s replace
159154
end

ocaml/libs/xapi-stdext/lib/xapi-stdext-std/xstringext.mli

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ module String : sig
1515
val isspace : char -> bool
1616
(** True if the character is whitespace *)
1717

18-
val escaped : ?rules:(char * string) list -> string -> string
19-
(** Backward-compatible string escaping, defaulting to the built-in
20-
OCaml string escaping but allowing an arbitrary mapping from characters
21-
to strings. *)
18+
val replaced : replace:(char -> string option) -> string -> string
19+
(** [replaced ~replacement str] applies [replace] to all characters in [str]
20+
and when it returns [Some rep] the character is replaced with [rep] in
21+
the resulting string *)
2222

2323
val split_f : (char -> bool) -> string -> string list
2424
(** Take a predicate and a string, return a list of strings separated by

ocaml/libs/xapi-stdext/lib/xapi-stdext-std/xstringext_test.ml

Lines changed: 1 addition & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -114,42 +114,6 @@ let test_rtrim =
114114
in
115115
("rtrim", List.map test spec)
116116

117-
(** Simple implementation of escaped for testing against *)
118-
let escaped_spec ?rules string =
119-
match rules with
120-
| None ->
121-
String.escaped string
122-
| Some rules ->
123-
let apply_rules char =
124-
match List.assoc_opt char rules with
125-
| None ->
126-
Seq.return char
127-
| Some replacement ->
128-
String.to_seq replacement
129-
in
130-
string |> String.to_seq |> Seq.concat_map apply_rules |> String.of_seq
131-
132-
let test_escaped =
133-
let open QCheck2 in
134-
(* Generator for escape rules: list of (char, string) mappings *)
135-
let gen_rules =
136-
let open Gen in
137-
let gen_rule = pair char (string_size (int_range 0 5) ~gen:char) in
138-
list gen_rule
139-
in
140-
(* Generator for test input: string and optional rules *)
141-
let gen_input = Gen.pair Gen.string (Gen.opt gen_rules) in
142-
let property (s, rules) =
143-
let expected = escaped_spec ?rules s in
144-
let actual = XString.escaped ?rules s in
145-
String.equal expected actual
146-
in
147-
let test =
148-
Test.make ~name:"escaped matches reference implementation" ~count:1000
149-
gen_input property
150-
in
151-
("escaped", [QCheck_alcotest.to_alcotest test])
152-
153117
let () =
154118
Alcotest.run "Xstringext"
155-
[test_split; test_split_f; test_has_substr; test_rtrim; test_escaped]
119+
[test_split; test_split_f; test_has_substr; test_rtrim]

ocaml/xapi-cli-server/cli_printer.ml

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,15 @@ let rec multi_line_record r =
6161

6262
(* Used to escape commas in --minimal mode *)
6363
let escape_commas x =
64-
(* Escaping rules: *)
65-
let rules = [(',', "\\,"); (* , -> \, *) ('\\', "\\\\") (* \ -> \\ *)] in
66-
Xapi_stdext_std.Xstringext.String.escaped ~rules x
64+
let replace = function
65+
| ',' ->
66+
Some "\\,"
67+
| '\\' ->
68+
Some "\\\\"
69+
| _ ->
70+
None
71+
in
72+
Xapi_stdext_std.Xstringext.String.replaced ~replace x
6773

6874
let make_printer sock minimal =
6975
let buffer = ref [] in

ocaml/xapi/console.ml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -265,9 +265,9 @@ let respond_console_limit_exceeded req s vm_id connected_users =
265265
error "The connected user list should not be empty." ;
266266
raise Failure
267267
| true, [user] ->
268-
Printf.sprintf "User '%s' is" (Http_svr.escape user)
268+
Printf.sprintf "User '%s' is" (Http_svr.escape_html user)
269269
| true, users ->
270-
let escaped_users = List.map Http_svr.escape users in
270+
let escaped_users = List.map Http_svr.escape_html users in
271271
Printf.sprintf "Users '%s' are" (String.concat ", " escaped_users)
272272
| false, _ ->
273273
"There're users"

0 commit comments

Comments
 (0)