Skip to content

Commit 590fc5d

Browse files
authored
Optimize with runtime.AddCleanup + more benchmarks (#12)
* experiment with 1.24 * add performance numbers * up
1 parent 282b72d commit 590fc5d

7 files changed

Lines changed: 314 additions & 27 deletions

File tree

.github/workflows/macos.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
test:
2323
strategy:
2424
matrix:
25-
go-version: [1.20.x]
25+
go-version: [1.24.x]
2626
platform: [macos-latest]
2727
runs-on: ${{ matrix.platform }}
2828
steps:

.github/workflows/ubuntu.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
test:
2323
strategy:
2424
matrix:
25-
go-version: [1.20.x]
25+
go-version: [1.24.x]
2626
platform: [ubuntu-latest]
2727
runs-on: ${{ matrix.platform }}
2828
steps:

README.md

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ The goada library provides support for the WHATWG URL standard in Go.
66

77
## Requirements
88

9-
- Go 1.20 or better.
9+
- Go 1.24 or better.
1010

1111

1212
### Examples
@@ -48,3 +48,62 @@ url.SetHash("goada")
4848
fmt.Println(url.Hash()) // prints #goada
4949
fmt.Println(url.Href()) // prints http://www.google.com/#goada
5050
```
51+
52+
## Performance
53+
54+
Benchmarks comparing URL parsing performance using the top 100k URLs dataset:
55+
56+
| Library | ns/op (full dataset) | ns/op per URL | WHATWG URL compliant ? |
57+
|--------------|----------------------|---------------|------------------------|
58+
| github.com/ada-url/goada | 2,206,395 | 22.0 | YES |
59+
| Go `net/url` | 1,937,154 | 19.4 | NO (sad face) |
60+
| github.com/nlnwa/whatwg-url | 13,916,373 | 139.0 | Yes |
61+
62+
Run `go test -bench BenchmarkTop100 -run -` to reproduce these results.
63+
64+
### Running Benchmarks
65+
66+
To run the URL parsing benchmarks:
67+
68+
1. **Clone the repository and navigate to the directory:**
69+
```bash
70+
git clone https://github.com/ada-url/goada.git
71+
cd goada
72+
```
73+
74+
2. **Install dependencies:**
75+
```bash
76+
go mod download
77+
```
78+
79+
3. **Run the benchmarks:**
80+
```bash
81+
# Run all benchmarks
82+
go test -bench=. -run=^$
83+
84+
# Run specific benchmark for top 100k URLs
85+
go test -bench=BenchmarkTop100 -run=^
86+
87+
# Run with more detailed output
88+
go test -bench=BenchmarkTop100 -benchmem -run=^
89+
```
90+
91+
4. **Compare with other libraries:**
92+
The benchmarks compare goada (Ada), Go's standard `net/url`, and `whatwg-url` library performance.
93+
94+
### Dataset
95+
96+
The benchmarks use the top 100k URLs dataset from [ada-url/url-various-datasets](https://github.com/ada-url/url-various-datasets), which contains the most popular URLs collected from real web traffic.
97+
98+
### Compliance Top 100
99+
100+
Testing with the top 100k URLs dataset shows that goada and whatwg-url produce identical normalized output for all URLs they both successfully parse. Go's standard `net/url` library produces different results in 1398 cases. The main differences are:
101+
102+
- **Query parameter encoding**: Go net/url does not encode spaces and special characters in query parameters, while Ada/whatwg-url do
103+
- **Path normalization**: Different handling of path components
104+
105+
Run `go test -run TestParserComparison -v` to see some differences.
106+
107+
## Citation
108+
109+
Yagiz Nizipli, Daniel Lemire, [Parsing Millions of URLs per Second](https://doi.org/10.1002/spe.3296), Software: Practice and Experience 54(5) May 2024.

go.mod

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
11
module github.com/ada-url/goada
22

3-
go 1.20
3+
go 1.24
4+
5+
require github.com/nlnwa/whatwg-url v0.6.2
6+
7+
require (
8+
github.com/bits-and-blooms/bitset v1.20.0 // indirect
9+
golang.org/x/net v0.34.0 // indirect
10+
golang.org/x/text v0.21.0 // indirect
11+
)

go.sum

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3MdfoPyRVU=
2+
github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
3+
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
4+
github.com/nlnwa/whatwg-url v0.6.2 h1:jU61lU2ig4LANydbEJmA2nPrtCGiKdtgT0rmMd2VZ/Q=
5+
github.com/nlnwa/whatwg-url v0.6.2/go.mod h1:x0FPXJzzOEieQtsBT/AKvbiBbQ46YlL6Xa7m02M1ECk=
6+
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
7+
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
8+
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
9+
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
10+
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
11+
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
12+
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
13+
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
14+
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
15+
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
16+
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
17+
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
18+
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
19+
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
20+
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
21+
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
22+
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
23+
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
24+
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
25+
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
26+
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
27+
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
28+
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
29+
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
30+
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
31+
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
32+
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
33+
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
34+
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
35+
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
36+
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
37+
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
38+
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
39+
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
40+
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
41+
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
42+
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
43+
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
44+
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
45+
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
46+
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
47+
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
48+
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
49+
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
50+
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
51+
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
52+
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
53+
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
54+
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
55+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
56+
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
57+
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
58+
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
59+
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
60+
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
61+
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
62+
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
63+
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
64+
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
65+
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
66+
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
67+
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
68+
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
69+
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
70+
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
71+
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

goada.go

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,17 @@ package goada
55
#cgo CXXFLAGS: -O3 -std=c++20
66
#include "ada_c.h"
77
8-
const char empty_string[] = "";
8+
9+
// ada_parse_and_validate combines parse + validity check in a single CGo call.
10+
// Returns the parsed URL, or NULL if invalid.
11+
static inline ada_url ada_parse_and_validate(const char* input, size_t length) {
12+
ada_url url = ada_parse(input, length);
13+
if (!ada_is_valid(url)) {
14+
ada_free(url);
15+
return NULL;
16+
}
17+
return url;
18+
}
919
*/
1020
import "C"
1121
import (
@@ -23,6 +33,7 @@ var ErrInvalidUrl = errors.New("invalid url")
2333

2434
type Url struct {
2535
cpointer C.ada_url
36+
cleanup runtime.Cleanup
2637
}
2738

2839
// parse the given string into a URL, a finalizer
@@ -31,14 +42,15 @@ func New(urlstring string) (*Url, error) {
3142
if len(urlstring) == 0 {
3243
return nil, ErrEmptyString
3344
}
34-
var answer *Url
35-
answer = &Url{C.ada_parse((*C.char)(unsafe.Pointer(unsafe.StringData(urlstring))), C.size_t(len(urlstring)))}
45+
cptr := C.ada_parse_and_validate((*C.char)(unsafe.Pointer(unsafe.StringData(urlstring))), C.size_t(len(urlstring)))
3646
runtime.KeepAlive(urlstring)
37-
if !C.ada_is_valid(answer.cpointer) {
38-
C.ada_free(answer.cpointer)
47+
if cptr == nil {
3948
return nil, ErrInvalidUrl
4049
}
41-
runtime.SetFinalizer(answer, free)
50+
answer := &Url{cpointer: cptr}
51+
answer.cleanup = runtime.AddCleanup(answer, func(cptr C.ada_url) {
52+
C.ada_free(cptr)
53+
}, cptr)
4254
return answer, nil
4355
}
4456

@@ -48,24 +60,20 @@ func NewWithBase(urlstring string, basestring string) (*Url, error) {
4860
if len(urlstring) == 0 || len(basestring) == 0 {
4961
return nil, ErrEmptyString
5062
}
51-
var answer *Url
52-
answer = &Url{C.ada_parse_with_base((*C.char)(unsafe.Pointer(unsafe.StringData(urlstring))), C.size_t(len(urlstring)), (*C.char)(unsafe.Pointer(unsafe.StringData(basestring))), C.size_t(len(basestring)))}
63+
cptr := C.ada_parse_with_base((*C.char)(unsafe.Pointer(unsafe.StringData(urlstring))), C.size_t(len(urlstring)), (*C.char)(unsafe.Pointer(unsafe.StringData(basestring))), C.size_t(len(basestring)))
5364
runtime.KeepAlive(urlstring)
5465
runtime.KeepAlive(basestring)
55-
if !C.ada_is_valid(answer.cpointer) {
56-
C.ada_free(answer.cpointer)
66+
if !C.ada_is_valid(cptr) {
67+
C.ada_free(cptr)
5768
return nil, ErrInvalidUrl
5869
}
59-
runtime.SetFinalizer(answer, free)
70+
answer := &Url{cpointer: cptr}
71+
answer.cleanup = runtime.AddCleanup(answer, func(cptr C.ada_url) {
72+
C.ada_free(cptr)
73+
}, cptr)
6074
return answer, nil
6175
}
6276

63-
func (rb *Url) Free() {
64-
// Clear the finalizer to avoid double frees
65-
runtime.SetFinalizer(rb, nil)
66-
free(rb)
67-
}
68-
6977
func (u *Url) Valid() bool {
7078
answer := bool(C.ada_is_valid(u.cpointer))
7179
runtime.KeepAlive(u)

0 commit comments

Comments
 (0)