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

exp/orderbook: Speed up path finding by changing edgeSet data structure #3965

Merged
merged 5 commits into from
Sep 29, 2021
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
4 changes: 3 additions & 1 deletion exp/orderbook/dfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,9 @@ func dfs(
return nil
}

for nextAssetString, venues := range state.venues(currentAssetString) {
edges := state.venues(currentAssetString)
for i := 0; i < len(edges); i++ {
nextAssetString, venues := edges[i].key, edges[i].value
if contains(visitedAssetStrings, nextAssetString) {
continue
}
Expand Down
90 changes: 56 additions & 34 deletions exp/orderbook/edges.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,48 +9,67 @@ import (
// edgeSet maintains a mapping of strings (asset keys) to a set of venues, which
// is composed of a sorted lists of offers and, optionally, a liquidity pool.
// The offers are sorted by ascending price (in terms of the buying asset).
type edgeSet map[string]Venues
type edgeSet []edge

type edge struct {
key string
value Venues
}

func (e edgeSet) find(key string) int {
for i := 0; i < len(e); i++ {
if e[i].key == key {
return i
}
}
return -1
}

// addOffer will insert the given offer into the edge set
func (e edgeSet) addOffer(key string, offer xdr.OfferEntry) {
func (e edgeSet) addOffer(key string, offer xdr.OfferEntry) edgeSet {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clever, great idea 👍

// The list of offers in a venue is sorted by cheapest to most expensive
// price to convert buyingAsset to sellingAsset
venues := e[key]
if len(venues.offers) == 0 {
e[key] = Venues{
offers: []xdr.OfferEntry{offer},
pool: venues.pool,
}
return
i := e.find(key)
if i < 0 {
return append(e, edge{key: key, value: Venues{offers: []xdr.OfferEntry{offer}}})
}

offers := e[i].value.offers
// find the smallest i such that Price of offers[i] > Price of offer
insertIndex := sort.Search(len(venues.offers), func(i int) bool {
return offer.Price.Cheaper(venues.offers[i].Price)
insertIndex := sort.Search(len(offers), func(j int) bool {
return offer.Price.Cheaper(offers[j].Price)
})

// then insert it into the slice (taken from Method 2 at
// https://github.com/golang/go/wiki/SliceTricks#insert).
offers := append(venues.offers, xdr.OfferEntry{}) // add to end
offers = append(offers, xdr.OfferEntry{}) // add to end
copy(offers[insertIndex+1:], offers[insertIndex:]) // shift right by 1
offers[insertIndex] = offer // insert

e[key] = Venues{offers: offers, pool: venues.pool}
e[i].value = Venues{offers: offers, pool: e[i].value.pool}
return e
}

// addPool makes `pool` a viable venue at `key`.
func (e edgeSet) addPool(key string, pool xdr.LiquidityPoolEntry) {
venues := e[key]
venues.pool = pool
e[key] = venues
func (e edgeSet) addPool(key string, pool xdr.LiquidityPoolEntry) edgeSet {
i := e.find(key)
if i < 0 {
return append(e, edge{key: key, value: Venues{pool: pool}})
}
e[i].value.pool = pool
return e
}

// removeOffer will delete the given offer from the edge set, returning whether
// or not the given offer was actually found.
func (e edgeSet) removeOffer(key string, offerID xdr.Int64) bool {
venues := e[key]
offers := venues.offers
func (e edgeSet) removeOffer(key string, offerID xdr.Int64) (edgeSet, bool) {
i := e.find(key)
if i < 0 {
return e, false
}

offers := e[i].value.offers
updatedOffers := offers
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we modify offers directly?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't want to modify offers while iterating through it in the for i, offer := range offers loop

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah good point, I see. It won't keep iterating after it's modified since it breaks immediately, but I can see why you would do this to avoid confusion when reading the code.

contains := false
for i, offer := range offers {
if offer.OfferId != offerID {
Expand All @@ -59,29 +78,32 @@ func (e edgeSet) removeOffer(key string, offerID xdr.Int64) bool {

// remove the entry in the slice at this location (taken from
// https://github.com/golang/go/wiki/SliceTricks#cut).
offers = append(offers[:i], offers[i+1:]...)
updatedOffers = append(offers[:i], offers[i+1:]...)
contains = true
break
}

if !contains {
return false
return e, false
}

if len(offers) == 0 && venues.pool.Body.ConstantProduct == nil {
delete(e, key)
} else {
venues.offers = offers
e[key] = venues
if len(updatedOffers) == 0 && e[i].value.pool.Body.ConstantProduct == nil {
return append(e[:i], e[i+1:]...), true
}

return true
e[i].value.offers = updatedOffers
return e, true
}

func (e edgeSet) removePool(key string) {
e[key] = Venues{offers: e[key].offers}
}
func (e edgeSet) removePool(key string) edgeSet {
i := e.find(key)
if i < 0 {
return e
}

if len(e[i].value.offers) == 0 {
return append(e[:i], e[i+1:]...)
}

func (e edgeSet) isEmpty(key string) bool {
return len(e[key].offers) == 0 && e[key].pool.Body.ConstantProduct == nil
e[i].value = Venues{offers: e[i].value.offers}
return e
}
56 changes: 17 additions & 39 deletions exp/orderbook/graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,8 @@ func (graph *OrderBookGraph) Offers() []xdr.OfferEntry {

var offers []xdr.OfferEntry
for _, edges := range graph.venuesForSellingAsset {
for _, venues := range edges {
offers = append(offers, venues.offers...)
for _, edge := range edges {
offers = append(offers, edge.value.offers...)
}
}

Expand Down Expand Up @@ -202,19 +202,8 @@ func (graph *OrderBookGraph) addOffer(offer xdr.OfferEntry) error {
buyingAsset: buying, sellingAsset: selling,
}

// First, ensure the internal structure of the graph is sound by creating
// empty venues if none exist yet.
if _, ok := graph.venuesForSellingAsset[selling]; !ok {
graph.venuesForSellingAsset[selling] = edgeSet{}
}

if _, ok := graph.venuesForBuyingAsset[buying]; !ok {
graph.venuesForBuyingAsset[buying] = edgeSet{}
}

// Now shove the new offer into them.
graph.venuesForSellingAsset[selling].addOffer(buying, offer)
graph.venuesForBuyingAsset[buying].addOffer(selling, offer)
graph.venuesForSellingAsset[selling] = graph.venuesForSellingAsset[selling].addOffer(buying, offer)
graph.venuesForBuyingAsset[buying] = graph.venuesForBuyingAsset[buying].addOffer(selling, offer)

return nil
}
Expand All @@ -226,23 +215,13 @@ func (graph *OrderBookGraph) addPool(pool xdr.LiquidityPoolEntry) {
x, y := getPoolAssets(pool)
graph.liquidityPools[tradingPair{x, y}] = pool

// Either there have already been offers added for the trading pair,
// or we need to create the internal map structure.
for _, asset := range []string{x, y} {
for _, table := range []map[string]edgeSet{
graph.venuesForBuyingAsset,
graph.venuesForSellingAsset,
} {
if _, ok := table[asset]; !ok {
table[asset] = edgeSet{}
}
}
for _, table := range []map[string]edgeSet{
graph.venuesForBuyingAsset,
graph.venuesForSellingAsset,
} {
table[x] = table[x].addPool(y, pool)
table[y] = table[y].addPool(x, pool)
}

graph.venuesForBuyingAsset[x].addPool(y, pool)
graph.venuesForBuyingAsset[y].addPool(x, pool)
graph.venuesForSellingAsset[x].addPool(y, pool)
graph.venuesForSellingAsset[y].addPool(x, pool)
}

// removeOffer deletes a given offer from the order book graph
Expand All @@ -256,18 +235,22 @@ func (graph *OrderBookGraph) removeOffer(offerID xdr.Int64) error {

if set, ok := graph.venuesForSellingAsset[pair.sellingAsset]; !ok {
return errOfferNotPresent
} else if !set.removeOffer(pair.buyingAsset, offerID) {
} else if set, ok = set.removeOffer(pair.buyingAsset, offerID); !ok {
return errOfferNotPresent
} else if len(set) == 0 {
delete(graph.venuesForSellingAsset, pair.sellingAsset)
} else {
graph.venuesForSellingAsset[pair.sellingAsset] = set
}

if set, ok := graph.venuesForBuyingAsset[pair.buyingAsset]; !ok {
return errOfferNotPresent
} else if !set.removeOffer(pair.sellingAsset, offerID) {
} else if set, ok = set.removeOffer(pair.sellingAsset, offerID); !ok {
return errOfferNotPresent
} else if len(set) == 0 {
delete(graph.venuesForBuyingAsset, pair.buyingAsset)
} else {
graph.venuesForBuyingAsset[pair.buyingAsset] = set
Shaptic marked this conversation as resolved.
Show resolved Hide resolved
}

return nil
Expand All @@ -287,12 +270,7 @@ func (graph *OrderBookGraph) removePool(pool xdr.LiquidityPoolEntry) {
graph.venuesForBuyingAsset,
graph.venuesForSellingAsset,
} {
if venues, ok := table[asset]; ok {
venues.removePool(otherAsset)
if venues.isEmpty(otherAsset) {
delete(venues, otherAsset)
}
} // should we panic on !ok?
table[asset] = table[asset].removePool(otherAsset)
}
}

Expand Down
51 changes: 50 additions & 1 deletion exp/orderbook/graph_benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package orderbook

import (
"context"
"encoding/binary"
"flag"
"io/ioutil"
"math"
Expand All @@ -19,9 +20,22 @@ import (
var (
// offersFile should contain a list of offers
// each line in the offers file is the base 64 encoding of an offer entry xdr
offersFile = flag.String("offers", "", "offers file generated by the dump-orderbook tool")
offersFile = flag.String("offers", "", "offers file generated by the dump-orderbook tool")
includePools = flag.Bool("pools", false, "include pools in the benchmark")
)

func assetFromString(s string) xdr.Asset {
if s == "native" {
return xdr.MustNewNativeAsset()
}
parts := strings.Split(s, "/")
asset, err := xdr.BuildAsset(parts[0], parts[2], parts[1])
if err != nil {
panic(err)
}
return asset
}

// loadGraphFromFile reads an offers file generated by the dump-orderbook tool
// and returns an orderbook built from those offers
func loadGraphFromFile(filePath string) (*OrderBookGraph, error) {
Expand All @@ -43,9 +57,44 @@ func loadGraphFromFile(filePath string) (*OrderBookGraph, error) {
return nil, err
}

if *includePools {
addLiquidityPools(graph)
if err := graph.Apply(2); err != nil {
return nil, err
}
}
return graph, nil
}

func addLiquidityPools(graph *OrderBookGraph) {
set := map[tradingPair]bool{}
for i, tp := range graph.tradingPairForOffer {
if !set[tp] {
a, b := assetFromString(tp.buyingAsset), assetFromString(tp.sellingAsset)
poolEntry := xdr.LiquidityPoolEntry{
LiquidityPoolId: xdr.PoolId{},
Body: xdr.LiquidityPoolEntryBody{
Type: xdr.LiquidityPoolTypeLiquidityPoolConstantProduct,
ConstantProduct: &xdr.LiquidityPoolEntryConstantProduct{
Params: xdr.LiquidityPoolConstantProductParameters{
AssetA: a,
AssetB: b,
Fee: 30,
},
ReserveA: 10000,
ReserveB: 10000,
TotalPoolShares: 1,
PoolSharesTrustLineCount: 1,
},
},
}
binary.PutVarint(poolEntry.LiquidityPoolId[:], int64(i))
graph.addPool(poolEntry)
set[tp] = true
}
}
}

type request struct {
src xdr.Asset
amt xdr.Int64
Expand Down
Loading