Skip to content
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
8 changes: 7 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ ifeq ($(GIT_DIRTY),)
GIT_DIRTY:=$(shell git diff --no-ext-diff 2> /dev/null | wc -l)
endif

GCFLAGS:=
ifeq ($(DEBUG),1)
GCFLAGS:=-gcflags="all=-N -l"
endif


.PHONY: all test coverage
all: build

Expand All @@ -29,7 +35,7 @@ build-coverage:

build:
$(GOBUILD) \
-ldflags="-X 'main.GitSHA1=$(GIT_SHA)' -X 'main.GitDirty=$(GIT_DIRTY)'" .
-ldflags="-X 'main.GitSHA1=$(GIT_SHA)'-X 'main.GitDirty=$(GIT_DIRTY)'" $(GCFLAGS) .

build-race:
$(GOBUILDRACE) \
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ require (
require (
github.com/tilinna/clock v1.0.2 // indirect
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6 // indirect
golang.org/x/sys v0.13.0 // indirect
)

replace github.com/redis/rueidis => github.com/filipecosta90/rueidis v0.0.0-20230927221707-2d17d4ee82e3
replace github.com/redis/rueidis => github.com/filipecosta90/rueidis v0.0.0-20231129020706-4fbfa4b6c663
36 changes: 29 additions & 7 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,30 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/filipecosta90/rueidis v0.0.0-20230927221707-2d17d4ee82e3 h1:slwoBsdbPe8JqOhlEaEZzkog/PLSwAQGuW3QtkIRsNM=
github.com/filipecosta90/rueidis v0.0.0-20230927221707-2d17d4ee82e3/go.mod h1:8B+r5wdnjwK3lTFml5VtxjzGOQAC+5UmujoD12pDrEo=
github.com/filipecosta90/rueidis v0.0.0-20231108003426-1ad576850596 h1:iUU5bL8Ztund2/NsvWp0agEL9wUX2UANMHgSEGBLiW0=
github.com/filipecosta90/rueidis v0.0.0-20231108003426-1ad576850596/go.mod h1:8EOzvsg3o5dUDitRj4vpsolUKkSIvFz88PeQnqwTVk0=
github.com/filipecosta90/rueidis v0.0.0-20231128153319-0edd62ab07b9 h1:UXuL0zzErGsv9DxQcabULmTUE551E94QIuRHv3usoRA=
github.com/filipecosta90/rueidis v0.0.0-20231128153319-0edd62ab07b9/go.mod h1:8EOzvsg3o5dUDitRj4vpsolUKkSIvFz88PeQnqwTVk0=
github.com/filipecosta90/rueidis v0.0.0-20231128154811-7d437c9947bd h1:M4+zNrbNM1ATsURMkFucsnkxxqTJrGFP+5SoE99WhU0=
github.com/filipecosta90/rueidis v0.0.0-20231128154811-7d437c9947bd/go.mod h1:8EOzvsg3o5dUDitRj4vpsolUKkSIvFz88PeQnqwTVk0=
github.com/filipecosta90/rueidis v0.0.0-20231128155605-75bd2dbb36ec h1:twpyLLucJ/KJGBNotqV7fJqmJIQDLlC/HJFbJZfjvhw=
github.com/filipecosta90/rueidis v0.0.0-20231128155605-75bd2dbb36ec/go.mod h1:8EOzvsg3o5dUDitRj4vpsolUKkSIvFz88PeQnqwTVk0=
github.com/filipecosta90/rueidis v0.0.0-20231128155953-c5a5fcdd82a2 h1:LtCI9QyVQVvsfWfPRomEtYyKIZQQuZk3d9crOXhwaqk=
github.com/filipecosta90/rueidis v0.0.0-20231128155953-c5a5fcdd82a2/go.mod h1:8EOzvsg3o5dUDitRj4vpsolUKkSIvFz88PeQnqwTVk0=
github.com/filipecosta90/rueidis v0.0.0-20231128160118-cdc18dddc040 h1:lYKygHbuIQtHNm1fUk1j8Y72EDkPY1jPTOOQ0Xv6kzM=
github.com/filipecosta90/rueidis v0.0.0-20231128160118-cdc18dddc040/go.mod h1:8EOzvsg3o5dUDitRj4vpsolUKkSIvFz88PeQnqwTVk0=
github.com/filipecosta90/rueidis v0.0.0-20231128160711-f6cbcd08afaa h1:jAgSUykSwKY2vx8vL663zcRfrVCCS4EQVcXOPBDQkWc=
github.com/filipecosta90/rueidis v0.0.0-20231128160711-f6cbcd08afaa/go.mod h1:8EOzvsg3o5dUDitRj4vpsolUKkSIvFz88PeQnqwTVk0=
github.com/filipecosta90/rueidis v0.0.0-20231128161500-345130b415c4 h1:AJYJUl6wRFEdMLCTNccARbHaNS4ia6pue294eamZD2A=
github.com/filipecosta90/rueidis v0.0.0-20231128161500-345130b415c4/go.mod h1:8EOzvsg3o5dUDitRj4vpsolUKkSIvFz88PeQnqwTVk0=
github.com/filipecosta90/rueidis v0.0.0-20231128162428-6ad1fe0d0899 h1:2d++oVwMuzkm5ptbX7ZZe30+0DpATQ6G1HS8diF4vAs=
github.com/filipecosta90/rueidis v0.0.0-20231128162428-6ad1fe0d0899/go.mod h1:8EOzvsg3o5dUDitRj4vpsolUKkSIvFz88PeQnqwTVk0=
github.com/filipecosta90/rueidis v0.0.0-20231128165119-1329d1c7101c h1:trMX28uA2CWYQ51YXdwXbMu3rnXRKm33uRkA3/wXdOY=
github.com/filipecosta90/rueidis v0.0.0-20231128165119-1329d1c7101c/go.mod h1:8EOzvsg3o5dUDitRj4vpsolUKkSIvFz88PeQnqwTVk0=
github.com/filipecosta90/rueidis v0.0.0-20231129020248-c71291edddb9 h1:K8aDQCc2tEdelLYaaZr2PaMLyfp7o2xZItqkUDggxj4=
github.com/filipecosta90/rueidis v0.0.0-20231129020248-c71291edddb9/go.mod h1:8EOzvsg3o5dUDitRj4vpsolUKkSIvFz88PeQnqwTVk0=
github.com/filipecosta90/rueidis v0.0.0-20231129020706-4fbfa4b6c663 h1:Ty498pDEDQ52dd7RS86uNyNN+x1H/4lRVVXwYjnEKnk=
github.com/filipecosta90/rueidis v0.0.0-20231129020706-4fbfa4b6c663/go.mod h1:8EOzvsg3o5dUDitRj4vpsolUKkSIvFz88PeQnqwTVk0=
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
Expand All @@ -24,11 +46,9 @@ github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lL
github.com/mediocregopher/radix/v4 v4.1.2 h1:Pj7XnNK5WuzzFy63g98pnccainAePK+aZNQRvxSvj2I=
github.com/mediocregopher/radix/v4 v4.1.2/go.mod h1:ajchozX/6ELmydxWeWM6xCFHVpZ4+67LXHOTOVR0nCE=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
github.com/onsi/gomega v1.28.0 h1:i2rg/p9n/UqIDAMFUJ6qIUUMcsqOuUHgbpbu235Vr1c=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/rueidis v1.0.19 h1:s65oWtotzlIFN8eMPhyYwxlwLR1lUdhza2KtWprKYSo=
github.com/redis/rueidis v1.0.19/go.mod h1:8B+r5wdnjwK3lTFml5VtxjzGOQAC+5UmujoD12pDrEo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
Expand All @@ -54,14 +74,16 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
Expand Down
37 changes: 26 additions & 11 deletions redis-bechmark-go.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ import (
"time"
)

func benchmarkRoutine(radixClient Client, ruedisClient rueidis.Client, useRuedis, useCSC, enableMultiExec bool, datapointsChan chan datapoint, continueOnError bool, cmdS [][]string, commandsCDF []float32, keyspacelen, datasize, number_samples uint64, loop bool, debug_level int, wg *sync.WaitGroup, keyplace, dataplace []int, readOnly []bool, useLimiter bool, rateLimiter *rate.Limiter, waitReplicas, waitReplicasMs int, cscDuration time.Duration) {
func benchmarkRoutine(radixClient Client, ruedisClient rueidis.Client, useRuedis, useCSC, enableMultiExec bool, datapointsChan chan datapoint, continueOnError bool, cmdS [][]string, commandsCDF []float32, keyspacelen, datasize, number_samples uint64, loop bool, debug_level int, wg *sync.WaitGroup, keyplace, dataplace []int, readOnly []bool, useLimiter bool, rateLimiter *rate.Limiter, waitReplicas, waitReplicasMs int, cacheOptions *rueidis.CacheOptions) {

defer wg.Done()
for i := 0; uint64(i) < number_samples || loop; i++ {
cmdPos := sample(commandsCDF)
Expand All @@ -32,15 +33,15 @@ func benchmarkRoutine(radixClient Client, ruedisClient rueidis.Client, useRuedis
time.Sleep(r.Delay())
}
if useRuedis {
sendCmdLogicRuedis(ruedisClient, newCmdS, enableMultiExec, datapointsChan, continueOnError, debug_level, useCSC, isReadOnly, cscDuration, waitReplicas, waitReplicasMs)
sendCmdLogicRuedis(ruedisClient, newCmdS, enableMultiExec, datapointsChan, continueOnError, debug_level, useCSC, isReadOnly, cacheOptions, waitReplicas, waitReplicasMs)
} else {
sendCmdLogicRadix(radixClient, newCmdS, enableMultiExec, key, datapointsChan, continueOnError, debug_level, waitReplicas, waitReplicasMs)

}
}
}

func sendCmdLogicRuedis(ruedisClient rueidis.Client, newCmdS []string, enableMultiExec bool, datapointsChan chan datapoint, continueOnError bool, debug_level int, useCSC, isReadOnly bool, cscDuration time.Duration, waitReplicas, waitReplicasMs int) {
func sendCmdLogicRuedis(ruedisClient rueidis.Client, newCmdS []string, enableMultiExec bool, datapointsChan chan datapoint, continueOnError bool, debug_level int, useCSC, isReadOnly bool, cacheOptions *rueidis.CacheOptions, waitReplicas, waitReplicasMs int) {
ctx := context.Background()
var startT time.Time
var endT time.Time
Expand All @@ -55,9 +56,15 @@ func sendCmdLogicRuedis(ruedisClient rueidis.Client, newCmdS []string, enableMul
}
}
if useCSC && isReadOnly {
startT = time.Now()
redisResult = ruedisClient.DoCache(ctx, arbitrary.Cache(), cscDuration)
endT = time.Now()
if cacheOptions.UseMultiExec && cacheOptions.UseServerPTTL {
startT = time.Now()
redisResult = ruedisClient.DoCache(ctx, arbitrary.Cache(), cacheOptions.ClientTTL)
endT = time.Now()
} else {
startT = time.Now()
redisResult = ruedisClient.DoCacheWithOptions(ctx, arbitrary.Cache(), *cacheOptions)
endT = time.Now()
}
} else if enableMultiExec {
cmds := make(rueidis.Commands, 0, 3)
cmds = append(cmds, ruedisClient.B().Multi().Build())
Expand Down Expand Up @@ -190,8 +197,13 @@ func main() {
betweenClientsDelay := flag.Duration("between-clients-duration", time.Millisecond*0, "Between each client creation, wait this time.")
version := flag.Bool("v", false, "Output version and exit")
verbose := flag.Bool("verbose", false, "Output verbose info")
cscEnabled := flag.Bool("csc", false, "Enable client side caching")
useRuedis := flag.Bool("rueidis", false, "Use rueidis as the vanilla underlying client.")
cscEnabled := flag.Bool("csc", false, "Enable client side caching")
// TODO: add this feature to check locking overhead
// cscDisableTrackInvalidations := flag.Bool("csc-disable-track-invalidations", false, "Disable CSC tracking")
cscUseMultiExec := flag.Bool("csc-use-multi-exec", false, "Use CSC wrapped in MULTI/EXEC")
// TODO: add this feature
// cscUseServerPTTL := flag.Bool("csc-use-server-pttl", false, "Use CSC wrapped in with PTTL expiration info")
cscDuration := flag.Duration("csc-ttl", time.Minute, "Client side cache ttl for cached entries")
clientKeepAlive := flag.Duration("client-keepalive", time.Minute, "Client keepalive")
cscSizeBytes := flag.Int("csc-per-client-bytes", rueidis.DefaultCacheBytes, "client side cache size that bind to each TCP connection to a single redis instance")
Expand Down Expand Up @@ -335,27 +347,30 @@ func main() {
AlwaysRESP2: alwaysRESP2,
DisableCache: !*cscEnabled,
BlockingPoolSize: 0,
PipelineMultiplex: 0,
PipelineMultiplex: -1,
RingScaleEachConn: 1,
ReadBufferEachConn: 1024,
WriteBufferEachConn: 1024,
CacheSizeEachConn: *cscSizeBytes,
OnInvalidations: invalidationFunction,
ForceSingleClient: !*clusterMode,
}
clientOptions.Dialer.KeepAlive = *clientKeepAlive
ruedisClient, err = rueidis.NewClient(clientOptions)
cacheOptions := rueidis.CacheOptions{UseMultiExec: *cscUseMultiExec, UseServerPTTL: *cscUseMultiExec, ClientTTL: *cscDuration}

if err != nil {
panic(err)
}
go benchmarkRoutine(radixStandalone, ruedisClient, *useRuedis, *cscEnabled, *multi, datapointsChan, *continueonerror, cmds, cdf, *keyspacelen, *datasize, samplesPerClient, *loop, int(*debug), &wg, cmdKeyplaceHolderPos, cmdDataplaceHolderPos, cmdReadOnly, useRateLimiter, rateLimiter, *waitReplicas, *waitReplicasMs, *cscDuration)
go benchmarkRoutine(radixStandalone, ruedisClient, *useRuedis, *cscEnabled, *multi, datapointsChan, *continueonerror, cmds, cdf, *keyspacelen, *datasize, samplesPerClient, *loop, int(*debug), &wg, cmdKeyplaceHolderPos, cmdDataplaceHolderPos, cmdReadOnly, useRateLimiter, rateLimiter, *waitReplicas, *waitReplicasMs, &cacheOptions)
} else {
// legacy radix code
if *clusterMode {
cluster = getOSSClusterConn(connectionStr, opts, 1)
go benchmarkRoutine(cluster, ruedisClient, *useRuedis, *cscEnabled, *multi, datapointsChan, *continueonerror, cmds, cdf, *keyspacelen, *datasize, samplesPerClient, *loop, int(*debug), &wg, cmdKeyplaceHolderPos, cmdDataplaceHolderPos, cmdReadOnly, useRateLimiter, rateLimiter, *waitReplicas, *waitReplicasMs, *cscDuration)
go benchmarkRoutine(cluster, ruedisClient, *useRuedis, *cscEnabled, *multi, datapointsChan, *continueonerror, cmds, cdf, *keyspacelen, *datasize, samplesPerClient, *loop, int(*debug), &wg, cmdKeyplaceHolderPos, cmdDataplaceHolderPos, cmdReadOnly, useRateLimiter, rateLimiter, *waitReplicas, *waitReplicasMs, nil)
} else {
radixStandalone = getStandaloneConn(connectionStr, opts, 1)
go benchmarkRoutine(radixStandalone, ruedisClient, *useRuedis, *cscEnabled, *multi, datapointsChan, *continueonerror, cmds, cdf, *keyspacelen, *datasize, samplesPerClient, *loop, int(*debug), &wg, cmdKeyplaceHolderPos, cmdDataplaceHolderPos, cmdReadOnly, useRateLimiter, rateLimiter, *waitReplicas, *waitReplicasMs, *cscDuration)
go benchmarkRoutine(radixStandalone, ruedisClient, *useRuedis, *cscEnabled, *multi, datapointsChan, *continueonerror, cmds, cdf, *keyspacelen, *datasize, samplesPerClient, *loop, int(*debug), &wg, cmdKeyplaceHolderPos, cmdDataplaceHolderPos, cmdReadOnly, useRateLimiter, rateLimiter, *waitReplicas, *waitReplicasMs, nil)
}
}

Expand Down
3 changes: 3 additions & 0 deletions redis-bechmark-go_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ func TestGecko(t *testing.T) {
{"run with key placeholder rueidis", 0, 10, 1000, []string{"-p", "6379", "-rueidis", "-c", "10", "-n", "1000", "-r", "10", "-rps", "10000", "HSET", "hash:__key__", "field", "value"}},
{"run with multiple commands", 0, 20, 1000, []string{"-p", "6379", "-c", "10", "-n", "1000", "-r", "10", "-rps", "10000", "-cmd", "HSET hash:__key__ field value", "-cmd-ratio", "0.5", "-cmd", "SET string:__key__ value", "-cmd-ratio", "0.5"}},
{"run with multiple commands rueidis", 0, 20, 1000, []string{"-p", "6379", "-rueidis", "-c", "10", "-n", "1000", "-r", "10", "-rps", "10000", "-cmd", "HSET hash:__key__ field value", "-cmd-ratio", "0.5", "-cmd", "SET string:__key__ value", "-cmd-ratio", "0.5"}},
{"run with multiple commands rueidis CSC", 0, 20, 1000, []string{"-p", "6379", "-rueidis", "-c", "10", "-n", "1000", "-r", "10", "-rps", "10000", "-cmd", "HSET hash:__key__ field value", "-csc", "-cmd-ratio", "0.5", "-cmd", "SET string:__key__ value", "-cmd-ratio", "0.5"}},
{"run with multiple commands rueidis CSC with MULTI/EXEC", 0, 20, 1000, []string{"-p", "6379", "-rueidis", "-c", "10", "-n", "1000", "-r", "10", "-rps", "10000", "-cmd", "HSET hash:__key__ field value", "-csc", "-csc-use-multi-exec", "-cmd-ratio", "0.5", "-cmd", "SET string:__key__ value", "-cmd-ratio", "0.5"}},

{"bad run", 2, 0, 0, []string{"-p", "xx"}},
}
host, password := getTestConnectionDetails()
Expand Down