Skip to content

Commit

Permalink
perf: optimize listpack performance and support lz4 compress (#41)
Browse files Browse the repository at this point in the history
* perf: optimize listpack removeNext cost

* perf: optimize listpack

* perf: encode entry in zipmap

* feat: add time event check oom

* feat: listpack use lz4 compress algprithm

* docs: update README benchmark

---------

Co-authored-by: guangzhixu <[email protected]>
  • Loading branch information
xgzlucario and satoshi-099 authored Jul 18, 2024
1 parent e9fcee0 commit e2a5eab
Show file tree
Hide file tree
Showing 29 changed files with 595 additions and 389 deletions.
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

0 comments on commit e2a5eab

Please sign in to comment.