Skip to content

Commit 1174f3a

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 81fb386 commit 1174f3a

10 files changed

Lines changed: 92 additions & 133 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
@@ -331,28 +331,22 @@ module Server = struct
331331
x.handlers []
332332
end
333333

334-
let escape uri =
335-
(* from xapi-stdext-std xstringext *)
336-
let escaped ~rules string =
337-
let aux h t =
338-
( if List.mem_assoc h rules then
339-
List.assoc h rules
340-
else
341-
Astring.String.of_char h
334+
let escape_html uri =
335+
Xapi_stdext_std.Xstringext.String.replaced
336+
~replace:(function
337+
| '<' ->
338+
Some "&lt;"
339+
| '>' ->
340+
Some "&gt;"
341+
| '\'' ->
342+
Some "&apos;"
343+
| '"' ->
344+
Some "&quot;"
345+
| '&' ->
346+
Some "&amp;"
347+
| _ ->
348+
None
342349
)
343-
:: t
344-
in
345-
String.concat "" (Astring.String.fold_right aux string [])
346-
in
347-
escaped
348-
~rules:
349-
[
350-
('<', "&lt;")
351-
; ('>', "&gt;")
352-
; ('\'', "&apos;")
353-
; ('"', "&quot;")
354-
; ('&', "&amp;")
355-
]
356350
uri
357351

358352
exception Generic_error of string
@@ -508,7 +502,7 @@ let read_request ?proxy_seen ~read_timeout ~total_timeout ~max_length fd =
508502
)
509503
| exc ->
510504
response_internal_error exc fd
511-
~extra:(escape (Printexc.to_string exc))
505+
~extra:(escape_html (Printexc.to_string exc))
512506
) ;
513507
(None, None)
514508

@@ -557,7 +551,7 @@ let handle_one (x : 'a Server.t) ss context req =
557551
)
558552
| exc ->
559553
response_internal_error ~req exc ss
560-
~extra:(escape (Printexc.to_string exc))
554+
~extra:(escape_html (Printexc.to_string exc))
561555
) ;
562556
!finished
563557

ocaml/libs/http-lib/http_svr.mli

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ val start :
6868

6969
val handle_one : 'a Server.t -> Unix.file_descr -> 'a -> Http.Request.t -> bool
7070

71+
val escape_html : string -> string
72+
(** Escapes HTML: replaces characters with their character references *)
73+
7174
exception Socket_not_found
7275

7376
val stop : socket -> unit

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
@@ -143,10 +143,5 @@ module String = struct
143143
) else
144144
s
145145

146-
let escaped ?rules s =
147-
match rules with
148-
| None ->
149-
String.escaped s
150-
| Some rules ->
151-
map_unlikely s (fun c -> List.assoc_opt c rules)
146+
let replaced ~replace s = map_unlikely s replace
152147
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
@@ -47,9 +47,15 @@ let rec multi_line_record r =
4747

4848
(* Used to escape commas in --minimal mode *)
4949
let escape_commas x =
50-
(* Escaping rules: *)
51-
let rules = [(',', "\\,"); (* , -> \, *) ('\\', "\\\\") (* \ -> \\ *)] in
52-
Xapi_stdext_std.Xstringext.String.escaped ~rules x
50+
let replace = function
51+
| ',' ->
52+
Some "\\,"
53+
| '\\' ->
54+
Some "\\\\"
55+
| _ ->
56+
None
57+
in
58+
Xapi_stdext_std.Xstringext.String.replaced ~replace x
5359

5460
let make_printer sock minimal =
5561
let buffer = ref [] in

ocaml/xapi/fileserver.ml

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,23 +22,11 @@ module D = Debug.Make (struct let name = "fileserver" end)
2222

2323
open D
2424

25-
let escape uri =
26-
Xstringext.escaped
27-
~rules:
28-
[
29-
('<', "&lt;")
30-
; ('>', "&gt;")
31-
; ('\'', "&apos;")
32-
; ('"', "&quot;")
33-
; ('&', "&amp;")
34-
]
35-
uri
36-
3725
let missing uri =
3826
"<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\"> <html><head> \
3927
<title>404 Not Found</title> </head><body> <h1>Not Found</h1> <p>The \
4028
requested URL "
41-
^ escape uri
29+
^ Http_svr.escape_html uri
4230
^ " was not found on this server.</p> <hr><address>Xapi \
4331
Server</address></body></html>"
4432

0 commit comments

Comments
 (0)