Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf: optimize listpack performance and support lz4 compress #41

Merged
merged 9 commits into from
Jul 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 68 additions & 60 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,36 @@
4. AOF 支持
5. 支持 18 种常用命令

### 原理介绍

**IO 多路复用**

IO多路复用是一种同时监听多个 socket 的技术,当一个或多个 socket 的读或写操作就绪时,程序会得到就绪事件通知,并进行相应的读写操作。常用的 IO多路复用机制有 select, poll, kqueue 等,在 Linux 平台上使用的是 epoll。

**AeLoop 事件循环**
### AELoop 事件循环

AeLoop(Async Event Loop) 是 Redis 的核心异步事件驱动机制,主要有以下部分:

1. FileEvent:使用 IO 多路复用处理网络 socket 上的读写事件。事件类型分为 `AE_READABLE` 和 `AE_WRIABLE`
1. FileEvent:使用 IO 多路复用处理网络 socket 上的读写事件。事件类型分为 `READABLE` 和 `WRIABLE`
2. TimeEvent:处理需要延迟执行或定时执行的任务,如每隔 `100ms` 进行过期淘汰
3. 当事件就绪时,通过该事件绑定的回调函数进行处理

在 rotom 内部实现中,还原了 Redis 中的 AeLoop 事件循环机制,具体来说:

1. 当一个新的 tcp 连接到达时,通过 `AcceptHandler` 获取该 socket 连接的 fd,并添加至事件循环,注册 `AE_READABLE` 读事件
2. 读事件就绪时,通过 `ReadQueryFromClient` 将数据读出至 `queryBuf`
1. 当一个新的 tcp 连接到达时,通过 `AcceptHandler` 获取连接的 socket fd,并添加至事件循环,注册读事件
2. 读事件就绪时,通过 `ReadQueryFromClient` 将缓冲数据读出至 `queryBuf`
3. 通过 `ProcessQueryBuf` 从 `queryBuf` 中解析并执行对应命令
4. 保存命令执行结果,并注册 socket fd 的 `AE_WRIABLE` 写事件
4. 保存命令执行结果,并注册 socket fd 的写事件
5. 写事件就绪时,通过 `SendReplyToClient` 将所有结果写回客户端,一个写事件可能一次性写回多个读事件的结果
6. 资源释放,并不断循环上述过程,直到服务关闭

### 数据结构

rotom 在数据结构上做了许多优化,当 hash 和 set 较小时,使用空间紧凑的 `zipmap` 和 `zipset` 以优化内存效率,并在适时使用 `lz4` 压缩算法压缩较冷数据,以进一步节省内存。

其中 `zipmap` 和 `zipset` 以及 `quicklist` 都基于 `listpack`, 这是 Redis 7.0+ 提出的新型压缩列表,支持正序及逆序遍历。

### 计划

- LRU 缓存及内存淘汰支持
- dict 渐进式哈希支持
- RDB 及 AOF Rewrite 支持
- 兼容更多常用命令

## 使用

**本机运行**
Expand All @@ -51,9 +58,10 @@ git clone https://github.com/xgzlucario/rotom

```
$ go run .
2024-06-15 16:41:22 DBG read cmd arguments config=config.json debug=true
2024-06-15 16:41:22 DBG running on port=6379
2024-06-15 16:41:22 DBG rotom server is ready to accept.
2024-07-18 23:37:13 INF current version buildTime=240718_233649+0800
2024-07-18 23:37:13 INF read cmd arguments config=/etc/rotom/config.json debug=false
2024-07-18 23:37:13 INF running on port=6379
2024-07-18 23:37:13 INF rotom server is ready to accept.
```

**容器运行**
Expand All @@ -62,7 +70,7 @@ $ go run .

```
REPOSITORY TAG IMAGE ID CREATED SIZE
rotom latest 22f42ce9ae0e 8 seconds ago 18.8MB
rotom latest 22f42ce9ae0e 8 seconds ago 20.5MB
```

然后启动容器:
Expand All @@ -88,56 +96,56 @@ cpu: AMD Ryzen 7 5800H with Radeon Graphics
redis-benchmark --csv
```

ROTOM

```
"test","rps","avg_latency_ms","min_latency_ms","p50_latency_ms","p95_latency_ms","p99_latency_ms","max_latency_ms"
"PING_INLINE","84674.01","0.316","0.136","0.303","0.399","0.567","2.967"
"PING_MBULK","85397.09","0.314","0.120","0.303","0.391","0.583","8.111"
"SET","83472.46","0.326","0.112","0.311","0.407","0.567","6.471"
"GET","87412.59","0.308","0.112","0.295","0.375","0.575","4.479"
"INCR","87032.20","0.313","0.120","0.303","0.383","0.503","5.519"
"LPUSH","35323.21","1.374","0.264","1.351","1.911","2.359","12.783"
"RPUSH","86805.56","0.317","0.104","0.303","0.391","0.575","8.383"
"LPOP","33990.48","1.429","0.280","1.431","1.951","2.423","7.479"
"RPOP","85984.52","0.314","0.160","0.303","0.383","0.551","5.367"
"SADD","86956.52","0.316","0.112","0.303","0.391","0.615","6.143"
"HSET","87183.96","0.319","0.088","0.303","0.391","0.615","7.031"
"SPOP","86281.27","0.313","0.096","0.303","0.383","0.527","9.247"
"ZADD","88495.58","0.314","0.112","0.303","0.383","0.551","6.207"
"ZPOPMIN","86132.64","0.313","0.112","0.303","0.383","0.543","7.135"
"LPUSH (needed to benchmark LRANGE)","34710.17","1.400","0.296","1.351","1.983","2.495","4.583"
"LRANGE_100 (first 100 elements)","18667.16","1.345","0.592","1.343","1.703","1.951","4.975"
"LRANGE_300 (first 300 elements)","9813.54","2.538","0.384","2.535","3.151","3.767","8.311"
"LRANGE_500 (first 500 elements)","6947.34","3.570","0.520","3.527","4.519","5.479","13.783"
"LRANGE_600 (first 600 elements)","5622.08","4.415","0.592","4.335","5.871","7.535","13.663"
"MSET (10 keys)","56947.61","0.531","0.232","0.463","0.959","1.567","7.535"
"XADD","75585.79","0.364","0.096","0.327","0.559","0.943","10.167"
"PING_INLINE","259067.36","0.102","0.024","0.103","0.119","0.199","1.855"
"PING_MBULK","262467.19","0.101","0.032","0.095","0.127","0.199","2.559"
"SET","266666.66","0.100","0.024","0.095","0.143","0.239","1.495"
"GET","261780.11","0.102","0.024","0.095","0.143","0.271","1.343"
"INCR","280898.88","0.095","0.032","0.095","0.119","0.255","0.999"
"LPUSH","286532.94","0.095","0.024","0.095","0.127","0.279","1.359"
"RPUSH","309597.50","0.089","0.032","0.087","0.119","0.223","1.839"
"LPOP","273224.03","0.097","0.032","0.095","0.119","0.191","1.855"
"RPOP","278551.53","0.094","0.032","0.095","0.111","0.183","1.303"
"SADD","281690.16","0.094","0.032","0.095","0.111","0.207","4.911"
"HSET","289017.34","0.092","0.024","0.087","0.111","0.207","3.447"
"SPOP","280112.06","0.095","0.024","0.095","0.119","0.215","1.559"
"ZADD","289855.06","0.091","0.024","0.087","0.111","0.207","0.983"
"ZPOPMIN","273224.03","0.097","0.032","0.095","0.111","0.175","0.975"
"LPUSH (needed to benchmark LRANGE)","292397.66","0.092","0.032","0.087","0.111","0.191","0.775"
"LRANGE_100 (first 100 elements)","42863.27","0.581","0.080","0.591","0.735","0.839","2.999"
"LRANGE_300 (first 300 elements)","20973.15","1.189","0.088","1.191","1.583","1.831","4.479"
"LRANGE_500 (first 500 elements)","13970.38","1.774","0.088","1.767","2.279","2.695","7.231"
"LRANGE_600 (first 600 elements)","11764.71","2.112","0.088","2.095","2.831","3.407","10.127"
"MSET (10 keys)","268096.53","0.103","0.024","0.095","0.167","0.279","1.079"
"XADD","261096.61","0.101","0.032","0.095","0.127","0.231","1.087"
```

REDIS
```bash
redis-benchmark --csv -P 10
```

```
"test","rps","avg_latency_ms","min_latency_ms","p50_latency_ms","p95_latency_ms","p99_latency_ms","max_latency_ms"
"PING_INLINE","76394.20","0.341","0.088","0.335","0.439","0.663","2.391"
"PING_MBULK","74349.44","0.349","0.104","0.343","0.455","0.623","3.087"
"SET","77639.75","0.335","0.080","0.327","0.423","0.551","3.079"
"GET","73475.39","0.353","0.080","0.343","0.471","0.631","3.551"
"INCR","75757.57","0.342","0.120","0.335","0.439","0.551","2.511"
"LPUSH","76804.91","0.337","0.096","0.327","0.431","0.567","3.135"
"RPUSH","76863.95","0.338","0.080","0.327","0.431","0.543","2.455"
"LPOP","76628.36","0.339","0.112","0.327","0.431","0.591","2.687"
"RPOP","75642.96","0.344","0.088","0.335","0.439","0.591","3.607"
"SADD","65231.57","0.399","0.096","0.375","0.591","0.831","3.495"
"HSET","71123.76","0.367","0.112","0.351","0.495","0.679","2.959"
"SPOP","74074.07","0.349","0.152","0.335","0.455","0.623","3.591"
"ZADD","74962.52","0.348","0.112","0.335","0.455","0.591","6.383"
"ZPOPMIN","71994.23","0.360","0.096","0.351","0.487","0.687","2.575"
"LPUSH (needed to benchmark LRANGE)","72833.21","0.359","0.104","0.343","0.503","0.711","2.095"
"LRANGE_100 (first 100 elements)","39494.47","0.647","0.192","0.631","0.847","1.079","3.375"
"LRANGE_300 (first 300 elements)","16920.47","1.482","0.296","1.463","1.927","2.263","4.319"
"LRANGE_500 (first 500 elements)","11713.72","2.130","0.408","2.071","2.831","3.439","9.655"
"LRANGE_600 (first 600 elements)","10833.06","2.298","0.432","2.271","2.847","3.247","6.015"
"MSET (10 keys)","84459.46","0.319","0.096","0.311","0.407","0.583","2.663"
"XADD","81433.22","0.323","0.104","0.311","0.415","0.519","2.503"
"PING_INLINE","1851851.75","0.142","0.064","0.143","0.175","0.215","1.511"
"PING_MBULK","1470588.12","0.177","0.080","0.175","0.239","0.463","1.543"
"SET","1724138.00","0.154","0.048","0.143","0.207","0.399","1.391"
"GET","1538461.62","0.169","0.056","0.167","0.231","0.343","0.831"
"INCR","2380952.50","0.118","0.048","0.111","0.223","0.367","0.479"
"LPUSH","2127659.75","0.182","0.088","0.159","0.327","0.495","0.855"
"RPUSH","1587301.50","0.290","0.080","0.279","0.463","0.615","0.951"
"LPOP","1886792.50","0.141","0.048","0.135","0.175","0.279","1.903"
"RPOP","1851851.75","0.140","0.064","0.135","0.175","0.255","0.775"
"SADD","2272727.25","0.122","0.048","0.111","0.183","0.367","1.951"
"HSET","2000000.00","0.228","0.080","0.207","0.335","0.583","3.015"
"SPOP","2173913.00","0.123","0.048","0.111","0.167","0.271","1.175"
"ZADD","2173913.00","0.201","0.080","0.175","0.359","0.703","5.639"
"ZPOPMIN","1515151.50","0.170","0.064","0.167","0.239","0.399","0.575"
"LPUSH (needed to benchmark LRANGE)","2083333.38","0.204","0.080","0.191","0.351","0.543","2.599"
"LRANGE_100 (first 100 elements)","76452.60","3.291","0.096","3.239","4.735","6.407","8.839"
"LRANGE_300 (first 300 elements)","26136.96","8.855","0.136","8.911","15.191","17.007","21.103"
"LRANGE_500 (first 500 elements)","20251.11","9.431","0.224","9.175","15.863","20.319","22.911"
"LRANGE_600 (first 600 elements)","14214.64","11.956","0.248","12.223","19.663","22.895","29.983"
"MSET (10 keys)","1449275.38","0.288","0.064","0.215","0.567","0.895","4.439"
"XADD","1694915.25","0.149","0.048","0.151","0.191","0.271","0.575"
```
33 changes: 10 additions & 23 deletions command.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ type Command struct {
// cmdTable is the list of all available commands.
var cmdTable []*Command = []*Command{
{"set", setCommand, 2, true},
{"mset", msetCommand, 2, true},
{"get", getCommand, 1, false},
{"incr", incrCommand, 1, true},
{"hset", hsetCommand, 3, true},
Expand All @@ -43,6 +42,9 @@ var cmdTable []*Command = []*Command{
{"ping", pingCommand, 0, false},
{"hgetall", hgetallCommand, 1, false},
{"lrange", lrangeCommand, 3, false},

// TODO
{"mset", todoCommand, 0, false},
{"zpopmin", todoCommand, 0, false},
{"xadd", todoCommand, 0, false},
}
Expand Down Expand Up @@ -72,7 +74,7 @@ func equalCommand(str, lowerText string) bool {

func (cmd *Command) processCommand(writer *RESPWriter, args []RESP) {
if len(args) < cmd.arity {
writer.WriteError(ErrWrongNumberArgs(cmd.name))
writer.WriteError(errInvalidArguments)
return
}
cmd.handler(writer, args)
Expand All @@ -89,20 +91,6 @@ func setCommand(writer *RESPWriter, args []RESP) {
writer.WriteString("OK")
}

func msetCommand(writer *RESPWriter, args []RESP) {
// check arguments number
if len(args)%2 == 1 {
writer.WriteError(ErrWrongNumberArgs("mset"))
return
}
for i := 0; i < len(args); i += 2 {
key := args[i].ToString()
value := args[i+1].Clone()
db.dict.Set(key, value)
}
writer.WriteString("OK")
}

func incrCommand(writer *RESPWriter, args []RESP) {
key := args[0].ToString()

Expand All @@ -115,13 +103,13 @@ func incrCommand(writer *RESPWriter, args []RESP) {

valBytes, ok := val.([]byte)
if !ok {
writer.WriteError(ErrWrongType)
writer.WriteError(errWrongType)
return
}

num, err := RESP(valBytes).ToInt()
if err != nil {
writer.WriteError(ErrParseInteger)
writer.WriteError(errParseInteger)
return
}
num++
Expand All @@ -143,17 +131,16 @@ func getCommand(writer *RESPWriter, args []RESP) {
if ok {
writer.WriteBulk(valBytes)
} else {
writer.WriteError(ErrWrongType)
writer.WriteError(errWrongType)
}
}

func hsetCommand(writer *RESPWriter, args []RESP) {
hash := args[0].ToString()
args = args[1:]

// check arguments number
if len(args)%2 == 1 {
writer.WriteError(ErrWrongNumberArgs("hset"))
writer.WriteError(errInvalidArguments)
return
}

Expand All @@ -180,7 +167,7 @@ func hgetCommand(writer *RESPWriter, args []RESP) {

hmap, err := fetchMap(hash)
if err != nil {
writer.WriteError(ErrWrongType)
writer.WriteError(errWrongType)
return
}

Expand Down Expand Up @@ -432,7 +419,7 @@ func fetch[T any](key string, new func() T, setnx ...bool) (v T, err error) {
if ok {
return v, nil
}
return v, ErrWrongType
return v, errWrongType
}
v = new()
if len(setnx) > 0 && setnx[0] {
Expand Down
6 changes: 3 additions & 3 deletions command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ func TestCommand(t *testing.T) {

rdb.Set(ctx, "notNum", "bar", 0)
_, err := rdb.Incr(ctx, "notNum").Result()
assert.Equal(err.Error(), ErrParseInteger.Error())
assert.Equal(err.Error(), errParseInteger.Error())
})

t.Run("hash", func(t *testing.T) {
Expand Down Expand Up @@ -102,10 +102,10 @@ func TestCommand(t *testing.T) {

// error
_, err := rdb.HSet(ctx, "map").Result()
assert.Equal(err.Error(), ErrWrongNumberArgs("hset").Error())
assert.Equal(err.Error(), errInvalidArguments.Error())

_, err = rdb.HSet(ctx, "map", "k1", "v1", "k2").Result()
assert.Equal(err.Error(), ErrWrongNumberArgs("hset").Error())
assert.Equal(err.Error(), errInvalidArguments.Error())
})

t.Run("list", func(t *testing.T) {
Expand Down
1 change: 1 addition & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ type Config struct {
Port int `json:"port"`
AppendOnly bool `json:"appendonly"`
AppendFileName string `json:"appendfilename"`
MaxMemory int `json:"maxMemory"`
}

func LoadConfig(path string) (config *Config, err error) {
Expand Down
16 changes: 5 additions & 11 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,13 @@ import (
)

var (
ErrWrongType = errors.New("WRONGTYPE Operation against a key holding the wrong kind of value")

ErrUnknownType = errors.New("ERR unknown value type")

ErrParseInteger = errors.New("ERR value is not an integer or out of range")

ErrCRLFNotFound = errors.New("ERR CRLF not found in line")
errWrongType = errors.New("WRONGTYPE Operation against a key holding the wrong kind of value")
errParseInteger = errors.New("ERR value is not an integer or out of range")
errCRLFNotFound = errors.New("ERR CRLF not found in line")
errInvalidArguments = errors.New("ERR invalid number of arguments")
errOOM = errors.New("ERR command not allowed when out of memory")
)

func ErrWrongNumberArgs(cmd string) error {
return fmt.Errorf("ERR wrong number of arguments for '%s' command", cmd)
}

func ErrUnknownCommand(cmd string) error {
return fmt.Errorf("ERR unknown command '%s'", cmd)
}
5 changes: 3 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ go 1.22
require (
github.com/cockroachdb/swiss v0.0.0-20240612210725-f4de07ae6964
github.com/deckarep/golang-set/v2 v2.6.0
github.com/klauspost/compress v1.17.9
github.com/influxdata/tdigest v0.0.1
github.com/pierrec/lz4/v4 v4.1.21
github.com/redis/go-redis/v9 v9.5.2
github.com/rs/zerolog v1.33.0
github.com/sakeven/RbTree v1.1.1
Expand All @@ -24,7 +25,7 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect
golang.org/x/exp v0.0.0-20240707233637-46b078467d37 // indirect
golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
18 changes: 14 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cu
github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ=
github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/influxdata/tdigest v0.0.1 h1:XpFptwYmnEKUqmkcDjrzffswZ3nvNeevbUSLPP/ZzIY=
github.com/influxdata/tdigest v0.0.1/go.mod h1:Z0kXnxzbTC2qrx4NaIzYkE1k66+6oEDQTvL95hQFh5Y=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
Expand All @@ -34,6 +37,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
Expand All @@ -51,14 +56,19 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tidwall/mmap v0.3.0 h1:XXt1YsiXCF5/UAu3pLbu6g7iulJ9jsbs6vt7UpiV0sY=
github.com/tidwall/mmap v0.3.0/go.mod h1:2/dNzF5zA+te/JVHfrqNLcRkb8LjdH3c80vYHFQEZRk=
golang.org/x/exp v0.0.0-20240707233637-46b078467d37 h1:uLDX+AfeFCct3a2C7uIWBKMJIR3CJMhcgfrUAqjRK6w=
golang.org/x/exp v0.0.0-20240707233637-46b078467d37/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7 h1:wDLEX9a7YQoKdKNQt88rtydkqDxeGaBUTnIYc3iG/mA=
golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gonum.org/v1/gonum v0.0.0-20181121035319-3f7ecaa7e8ca h1:PupagGYwj8+I4ubCxcmcBRk3VlUWtTg5huQpZR9flmE=
gonum.org/v1/gonum v0.0.0-20181121035319-3f7ecaa7e8ca/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
gonum.org/v1/netlib v0.0.0-20181029234149-ec6d1f5cefe6/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
Expand Down
Loading
Loading