Merge pull request #1315 from gavin-ts/grid-adjustments

improve grid performance with similarly sized objects
This commit is contained in:
gavin-ts 2023-05-10 12:55:09 -07:00 committed by GitHub
commit c6820c89cc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 39668 additions and 28 deletions

View file

@ -4,5 +4,6 @@
- Use shape specific sizing for grid containers [#1294](https://github.com/terrastruct/d2/pull/1294)
- Watch mode browser uses an error favicon to easily indicate compiler errors. Thanks @sinyo-matu ! [#1240](https://github.com/terrastruct/d2/pull/1240)
- Improves grid layout performance when there are many similarly sized shapes. [#1315](https://github.com/terrastruct/d2/pull/1315)
#### Bugfixes ⛑️

View file

@ -0,0 +1,13 @@
package d2grid
const (
// don't consider layouts with rows longer than targetSize*1.2 or shorter than targetSize/1.2
STARTING_THRESHOLD = 1.2
// next try layouts with a 25% larger threshold
THRESHOLD_STEP_SIZE = 0.25
MIN_THRESHOLD_ATTEMPTS = 1
MAX_THRESHOLD_ATTEMPTS = 3
ATTEMPT_LIMIT = 100_000
SKIP_LIMIT = 10_000_000
)

View file

@ -1,6 +1,7 @@
package d2grid
import (
"bytes"
"context"
"fmt"
"math"
@ -466,6 +467,7 @@ func (gd *gridDiagram) layoutDynamic(g *d2graph.Graph, obj *d2graph.Object) {
// generate the best layout of objects aiming for each row to be the targetSize width
// if columns is true, each column aims to have the targetSize height
func (gd *gridDiagram) getBestLayout(targetSize float64, columns bool) [][]*d2graph.Object {
debug := false
var nCuts int
if columns {
nCuts = gd.columns - 1
@ -476,6 +478,23 @@ func (gd *gridDiagram) getBestLayout(targetSize float64, columns bool) [][]*d2gr
return genLayout(gd.objects, nil)
}
var bestLayout [][]*d2graph.Object
bestDist := math.MaxFloat64
fastIsBest := false
// try fast layout algorithm as a baseline
if fastLayout := gd.fastLayout(targetSize, nCuts, columns); fastLayout != nil {
dist := getDistToTarget(fastLayout, targetSize, float64(gd.horizontalGap), float64(gd.verticalGap), columns)
if debug {
fmt.Printf("fast dist %v dist per row %v\n", dist, dist/(float64(nCuts)+1))
}
if dist == 0 {
return fastLayout
}
bestDist = dist
bestLayout = fastLayout
fastIsBest = true
}
var gap float64
if columns {
gap = float64(gd.verticalGap)
@ -490,17 +509,24 @@ func (gd *gridDiagram) getBestLayout(targetSize float64, columns bool) [][]*d2gr
}
}
debug := false
sizes := []float64{}
for _, obj := range gd.objects {
size := getSize(obj)
sizes = append(sizes, size)
}
sd := stddev(sizes)
if debug {
fmt.Printf("sizes (%d): %v\n", len(sizes), sizes)
fmt.Printf("std dev: %v; targetSize %v\n", sd, targetSize)
}
skipCount := 0
count := 0
// quickly eliminate bad row groupings
startingCache := make(map[int]bool)
// try to find a layout with all rows within 1.2*targetSize
// skip options with a row that is 1.2*longer or shorter
// Note: we want a low threshold to explore good options within attemptLimit,
// but the best option may require a few rows that are far from the target size.
okThreshold := 1.2
// if we don't find a layout try 25% larger threshold
thresholdStep := 0.25
okThreshold := STARTING_THRESHOLD
rowOk := func(row []*d2graph.Object, starting bool) (ok bool) {
if starting {
// we can cache results from starting positions since they repeat and don't change
@ -523,21 +549,24 @@ func (gd *gridDiagram) getBestLayout(targetSize float64, columns bool) [][]*d2gr
// if multiple nodes are too big, it isn't ok. but a single node can't shrink so only check here
if rowSize > okThreshold*targetSize {
skipCount++
if skipCount >= SKIP_LIMIT {
// there may even be too many to skip
return true
}
return false
}
}
// row is too small to be good overall
if rowSize < targetSize/okThreshold {
skipCount++
if skipCount >= SKIP_LIMIT {
return true
}
return false
}
return true
}
var bestLayout [][]*d2graph.Object
bestDist := math.MaxFloat64
count := 0
attemptLimit := 100_000
// get all options for where to place these cuts, preferring later cuts over earlier cuts
// with 5 objects and 2 cuts we have these options:
// . A B C │ D │ E <- these cuts would produce: ┌A─┐ ┌B─┐ ┌C─┐
@ -553,33 +582,94 @@ func (gd *gridDiagram) getBestLayout(targetSize float64, columns bool) [][]*d2gr
if dist < bestDist {
bestLayout = layout
bestDist = dist
fastIsBest = false
} else if fastIsBest && dist == bestDist {
// prefer ordered search solution to fast layout solution
bestLayout = layout
fastIsBest = false
}
count++
// with few objects we can try all options to get best result but this won't scale, so only try up to 100k options
return count >= attemptLimit
return count >= ATTEMPT_LIMIT || skipCount >= SKIP_LIMIT
}
// try at least 3 different okThresholds
for i := 0; i < 3 || bestLayout == nil; i++ {
// try number of different okThresholds depending on std deviation of sizes
thresholdAttempts := int(math.Ceil(sd))
if thresholdAttempts < MIN_THRESHOLD_ATTEMPTS {
thresholdAttempts = MIN_THRESHOLD_ATTEMPTS
} else if thresholdAttempts > MAX_THRESHOLD_ATTEMPTS {
thresholdAttempts = MAX_THRESHOLD_ATTEMPTS
}
for i := 0; i < thresholdAttempts || bestLayout == nil; i++ {
count = 0.
skipCount = 0.
iterDivisions(gd.objects, nCuts, tryDivision, rowOk)
okThreshold += thresholdStep
okThreshold += THRESHOLD_STEP_SIZE
if debug {
fmt.Printf("increasing ok threshold to %v\n", okThreshold)
fmt.Printf("count %d, skip count %d, bestDist %v increasing ok threshold to %v\n", count, skipCount, bestDist, okThreshold)
}
startingCache = make(map[int]bool)
count = 0.
}
if debug {
fmt.Printf("final count %d, skip count %d\n", count, skipCount)
if skipCount == 0 {
// threshold isn't skipping anything so increasing it won't help
break
}
// okThreshold isn't high enough yet, we skipped every option so don't count it
if count == 0 && thresholdAttempts < MAX_THRESHOLD_ATTEMPTS {
thresholdAttempts++
}
}
if debug {
fmt.Printf("best layout: %v\n", layoutString(bestLayout, sizes))
}
return bestLayout
}
func sum(values []float64) float64 {
s := 0.
for _, v := range values {
s += v
}
return s
}
func avg(values []float64) float64 {
return sum(values) / float64(len(values))
}
func variance(values []float64) float64 {
mean := avg(values)
total := 0.
for _, value := range values {
dev := mean - value
total += dev * dev
}
return total / float64(len(values))
}
func stddev(values []float64) float64 {
return math.Sqrt(variance(values))
}
func (gd *gridDiagram) fastLayout(targetSize float64, nCuts int, columns bool) (layout [][]*d2graph.Object) {
var gap float64
if columns {
gap = float64(gd.verticalGap)
} else {
gap = float64(gd.horizontalGap)
}
// try fast layout algorithm, see if it is better than first 1mil attempts
debt := 0.
fastDivision := make([]int, 0, nCuts)
rowSize := 0.
for i := 0; i < len(gd.objects); i++ {
o := gd.objects[i]
size := getSize(o)
var size float64
if columns {
size = o.Height
} else {
size = o.Width
}
if rowSize == 0 {
if size > targetSize-debt {
fastDivision = append(fastDivision, i-1)
@ -602,14 +692,23 @@ func (gd *gridDiagram) getBestLayout(targetSize float64, columns bool) [][]*d2gr
}
}
if len(fastDivision) == nCuts {
layout := genLayout(gd.objects, fastDivision)
dist := getDistToTarget(layout, targetSize, float64(gd.horizontalGap), float64(gd.verticalGap), columns)
if dist < bestDist {
bestLayout = layout
bestDist = dist
}
layout = genLayout(gd.objects, fastDivision)
}
return bestLayout
return layout
}
func layoutString(layout [][]*d2graph.Object, sizes []float64) string {
buf := &bytes.Buffer{}
i := 0
fmt.Fprintf(buf, "[\n")
for _, r := range layout {
vals := sizes[i : i+len(r)]
fmt.Fprintf(buf, "%v:\t%v\n", sum(vals), vals)
i += len(r)
}
fmt.Fprintf(buf, "]\n")
return buf.String()
}
// process current division, return true to stop iterating

View file

@ -2720,6 +2720,7 @@ scenarios: {
loadFromFile(t, "grid_even"),
loadFromFile(t, "ent2d2_basic"),
loadFromFile(t, "ent2d2_right"),
loadFromFile(t, "grid_large_checkered"),
}
runa(t, tcs)

View file

@ -0,0 +1,446 @@
classes.BLACK: {style.fill: black; style.stroke-width: 0}
classes.WHITE: {style.fill: white; style.stroke-width: 0}
# grid-rows: 21
grid-columns: 21
grid-gap: 0
1: "" {class: BLACK}
2: "" {class: WHITE}
3: "" {class: BLACK}
4: "" {class: WHITE}
5: "" {class: BLACK}
6: "" {class: WHITE}
7: "" {class: BLACK}
8: "" {class: WHITE}
9: "" {class: BLACK}
10: "" {class: WHITE}
11: "" {class: BLACK}
12: "" {class: WHITE}
13: "" {class: BLACK}
14: "" {class: WHITE}
15: "" {class: BLACK}
16: "" {class: WHITE}
17: "" {class: BLACK}
18: "" {class: WHITE}
19: "" {class: BLACK}
20: "" {class: WHITE}
21: "" {class: BLACK}
22: "" {class: WHITE}
23: "" {class: BLACK}
24: "" {class: WHITE}
25: "" {class: BLACK}
26: "" {class: WHITE}
27: "" {class: BLACK}
28: "" {class: WHITE}
29: "" {class: BLACK}
30: "" {class: WHITE}
31: "" {class: BLACK}
32: "" {class: WHITE}
33: "" {class: BLACK}
34: "" {class: WHITE}
35: "" {class: BLACK}
36: "" {class: WHITE}
37: "" {class: BLACK}
38: "" {class: WHITE}
39: "" {class: BLACK}
40: "" {class: WHITE}
41: "" {class: BLACK}
42: "" {class: WHITE}
43: "" {class: BLACK}
44: "" {class: WHITE}
45: "" {class: BLACK}
46: "" {class: WHITE}
47: "" {class: BLACK}
48: "" {class: WHITE}
49: "" {class: BLACK}
50: "" {class: WHITE}
51: "" {class: BLACK}
52: "" {class: WHITE}
53: "" {class: BLACK}
54: "" {class: WHITE}
55: "" {class: BLACK}
56: "" {class: WHITE}
57: "" {class: BLACK}
58: "" {class: WHITE}
59: "" {class: BLACK}
60: "" {class: WHITE}
61: "" {class: BLACK}
62: "" {class: WHITE}
63: "" {class: BLACK}
64: "" {class: WHITE}
65: "" {class: BLACK}
66: "" {class: WHITE}
67: "" {class: BLACK}
68: "" {class: WHITE}
69: "" {class: BLACK}
70: "" {class: WHITE}
71: "" {class: BLACK}
72: "" {class: WHITE}
73: "" {class: BLACK}
74: "" {class: WHITE}
75: "" {class: BLACK}
76: "" {class: WHITE}
77: "" {class: BLACK}
78: "" {class: WHITE}
79: "" {class: BLACK}
80: "" {class: WHITE}
81: "" {class: BLACK}
82: "" {class: WHITE}
83: "" {class: BLACK}
84: "" {class: WHITE}
85: "" {class: BLACK}
86: "" {class: WHITE}
87: "" {class: BLACK}
88: "" {class: WHITE}
89: "" {class: BLACK}
90: "" {class: WHITE}
91: "" {class: BLACK}
92: "" {class: WHITE}
93: "" {class: BLACK}
94: "" {class: WHITE}
95: "" {class: BLACK}
96: "" {class: WHITE}
97: "" {class: BLACK}
98: "" {class: WHITE}
99: "" {class: BLACK}
100: "" {class: WHITE}
101: "" {class: BLACK}
102: "" {class: WHITE}
103: "" {class: BLACK}
104: "" {class: WHITE}
105: "" {class: BLACK}
106: "" {class: WHITE}
107: "" {class: BLACK}
108: "" {class: WHITE}
109: "" {class: BLACK}
110: "" {class: WHITE}
111: "" {class: BLACK}
112: "" {class: WHITE}
113: "" {class: BLACK}
114: "" {class: WHITE}
115: "" {class: BLACK}
116: "" {class: WHITE}
117: "" {class: BLACK}
118: "" {class: WHITE}
119: "" {class: BLACK}
120: "" {class: WHITE}
121: "" {class: BLACK}
122: "" {class: WHITE}
123: "" {class: BLACK}
124: "" {class: WHITE}
125: "" {class: BLACK}
126: "" {class: WHITE}
127: "" {class: BLACK}
128: "" {class: WHITE}
129: "" {class: BLACK}
130: "" {class: WHITE}
131: "" {class: BLACK}
132: "" {class: WHITE}
133: "" {class: BLACK}
134: "" {class: WHITE}
135: "" {class: BLACK}
136: "" {class: WHITE}
137: "" {class: BLACK}
138: "" {class: WHITE}
139: "" {class: BLACK}
140: "" {class: WHITE}
141: "" {class: BLACK}
142: "" {class: WHITE}
143: "" {class: BLACK}
144: "" {class: WHITE}
145: "" {class: BLACK}
146: "" {class: WHITE}
147: "" {class: BLACK}
148: "" {class: WHITE}
149: "" {class: BLACK}
150: "" {class: WHITE}
151: "" {class: BLACK}
152: "" {class: WHITE}
153: "" {class: BLACK}
154: "" {class: WHITE}
155: "" {class: BLACK}
156: "" {class: WHITE}
157: "" {class: BLACK}
158: "" {class: WHITE}
159: "" {class: BLACK}
160: "" {class: WHITE}
161: "" {class: BLACK}
162: "" {class: WHITE}
163: "" {class: BLACK}
164: "" {class: WHITE}
165: "" {class: BLACK}
166: "" {class: WHITE}
167: "" {class: BLACK}
168: "" {class: WHITE}
169: "" {class: BLACK}
170: "" {class: WHITE}
171: "" {class: BLACK}
172: "" {class: WHITE}
173: "" {class: BLACK}
174: "" {class: WHITE}
175: "" {class: BLACK}
176: "" {class: WHITE}
177: "" {class: BLACK}
178: "" {class: WHITE}
179: "" {class: BLACK}
180: "" {class: WHITE}
181: "" {class: BLACK}
182: "" {class: WHITE}
183: "" {class: BLACK}
184: "" {class: WHITE}
185: "" {class: BLACK}
186: "" {class: WHITE}
187: "" {class: BLACK}
188: "" {class: WHITE}
189: "" {class: BLACK}
190: "" {class: WHITE}
191: "" {class: BLACK}
192: "" {class: WHITE}
193: "" {class: BLACK}
194: "" {class: WHITE}
195: "" {class: BLACK}
196: "" {class: WHITE}
197: "" {class: BLACK}
198: "" {class: WHITE}
199: "" {class: BLACK}
200: "" {class: WHITE}
201: "" {class: BLACK}
202: "" {class: WHITE}
203: "" {class: BLACK}
204: "" {class: WHITE}
205: "" {class: BLACK}
206: "" {class: WHITE}
207: "" {class: BLACK}
208: "" {class: WHITE}
209: "" {class: BLACK}
210: "" {class: WHITE}
211: "" {class: BLACK}
212: "" {class: WHITE}
213: "" {class: BLACK}
214: "" {class: WHITE}
215: "" {class: BLACK}
216: "" {class: WHITE}
217: "" {class: BLACK}
218: "" {class: WHITE}
219: "" {class: BLACK}
220: "" {class: WHITE}
221: "" {class: BLACK}
222: "" {class: WHITE}
223: "" {class: BLACK}
224: "" {class: WHITE}
225: "" {class: BLACK}
226: "" {class: WHITE}
227: "" {class: BLACK}
228: "" {class: WHITE}
229: "" {class: BLACK}
230: "" {class: WHITE}
231: "" {class: BLACK}
232: "" {class: WHITE}
233: "" {class: BLACK}
234: "" {class: WHITE}
235: "" {class: BLACK}
236: "" {class: WHITE}
237: "" {class: BLACK}
238: "" {class: WHITE}
239: "" {class: BLACK}
240: "" {class: WHITE}
241: "" {class: BLACK}
242: "" {class: WHITE}
243: "" {class: BLACK}
244: "" {class: WHITE}
245: "" {class: BLACK}
246: "" {class: WHITE}
247: "" {class: BLACK}
248: "" {class: WHITE}
249: "" {class: BLACK}
250: "" {class: WHITE}
251: "" {class: BLACK}
252: "" {class: WHITE}
253: "" {class: BLACK}
254: "" {class: WHITE}
255: "" {class: BLACK}
256: "" {class: WHITE}
257: "" {class: BLACK}
258: "" {class: WHITE}
259: "" {class: BLACK}
260: "" {class: WHITE}
261: "" {class: BLACK}
262: "" {class: WHITE}
263: "" {class: BLACK}
264: "" {class: WHITE}
265: "" {class: BLACK}
266: "" {class: WHITE}
267: "" {class: BLACK}
268: "" {class: WHITE}
269: "" {class: BLACK}
270: "" {class: WHITE}
271: "" {class: BLACK}
272: "" {class: WHITE}
273: "" {class: BLACK}
274: "" {class: WHITE}
275: "" {class: BLACK}
276: "" {class: WHITE}
277: "" {class: BLACK}
278: "" {class: WHITE}
279: "" {class: BLACK}
280: "" {class: WHITE}
281: "" {class: BLACK}
282: "" {class: WHITE}
283: "" {class: BLACK}
284: "" {class: WHITE}
285: "" {class: BLACK}
286: "" {class: WHITE}
287: "" {class: BLACK}
288: "" {class: WHITE}
289: "" {class: BLACK}
290: "" {class: WHITE}
291: "" {class: BLACK}
292: "" {class: WHITE}
293: "" {class: BLACK}
294: "" {class: WHITE}
295: "" {class: BLACK}
296: "" {class: WHITE}
297: "" {class: BLACK}
298: "" {class: WHITE}
299: "" {class: BLACK}
300: "" {class: WHITE}
301: "" {class: BLACK}
302: "" {class: WHITE}
303: "" {class: BLACK}
304: "" {class: WHITE}
305: "" {class: BLACK}
306: "" {class: WHITE}
307: "" {class: BLACK}
308: "" {class: WHITE}
309: "" {class: BLACK}
310: "" {class: WHITE}
311: "" {class: BLACK}
312: "" {class: WHITE}
313: "" {class: BLACK}
314: "" {class: WHITE}
315: "" {class: BLACK}
316: "" {class: WHITE}
317: "" {class: BLACK}
318: "" {class: WHITE}
319: "" {class: BLACK}
320: "" {class: WHITE}
321: "" {class: BLACK}
322: "" {class: WHITE}
323: "" {class: BLACK}
324: "" {class: WHITE}
325: "" {class: BLACK}
326: "" {class: WHITE}
327: "" {class: BLACK}
328: "" {class: WHITE}
329: "" {class: BLACK}
330: "" {class: WHITE}
331: "" {class: BLACK}
332: "" {class: WHITE}
333: "" {class: BLACK}
334: "" {class: WHITE}
335: "" {class: BLACK}
336: "" {class: WHITE}
337: "" {class: BLACK}
338: "" {class: WHITE}
339: "" {class: BLACK}
340: "" {class: WHITE}
341: "" {class: BLACK}
342: "" {class: WHITE}
343: "" {class: BLACK}
344: "" {class: WHITE}
345: "" {class: BLACK}
346: "" {class: WHITE}
347: "" {class: BLACK}
348: "" {class: WHITE}
349: "" {class: BLACK}
350: "" {class: WHITE}
351: "" {class: BLACK}
352: "" {class: WHITE}
353: "" {class: BLACK}
354: "" {class: WHITE}
355: "" {class: BLACK}
356: "" {class: WHITE}
357: "" {class: BLACK}
358: "" {class: WHITE}
359: "" {class: BLACK}
360: "" {class: WHITE}
361: "" {class: BLACK}
362: "" {class: WHITE}
363: "" {class: BLACK}
364: "" {class: WHITE}
365: "" {class: BLACK}
366: "" {class: WHITE}
367: "" {class: BLACK}
368: "" {class: WHITE}
369: "" {class: BLACK}
370: "" {class: WHITE}
371: "" {class: BLACK}
372: "" {class: WHITE}
373: "" {class: BLACK}
374: "" {class: WHITE}
375: "" {class: BLACK}
376: "" {class: WHITE}
377: "" {class: BLACK}
378: "" {class: WHITE}
379: "" {class: BLACK}
380: "" {class: WHITE}
381: "" {class: BLACK}
382: "" {class: WHITE}
383: "" {class: BLACK}
384: "" {class: WHITE}
385: "" {class: BLACK}
386: "" {class: WHITE}
387: "" {class: BLACK}
388: "" {class: WHITE}
389: "" {class: BLACK}
390: "" {class: WHITE}
391: "" {class: BLACK}
392: "" {class: WHITE}
393: "" {class: BLACK}
394: "" {class: WHITE}
395: "" {class: BLACK}
396: "" {class: WHITE}
397: "" {class: BLACK}
398: "" {class: WHITE}
399: "" {class: BLACK}
400: "" {class: WHITE}
401: "" {class: BLACK}
402: "" {class: WHITE}
403: "" {class: BLACK}
404: "" {class: WHITE}
405: "" {class: BLACK}
406: "" {class: WHITE}
407: "" {class: BLACK}
408: "" {class: WHITE}
409: "" {class: BLACK}
410: "" {class: WHITE}
411: "" {class: BLACK}
412: "" {class: WHITE}
413: "" {class: BLACK}
414: "" {class: WHITE}
415: "" {class: BLACK}
416: "" {class: WHITE}
417: "" {class: BLACK}
418: "" {class: WHITE}
419: "" {class: BLACK}
420: "" {class: WHITE}
421: "" {class: BLACK}
422: "" {class: WHITE}
423: "" {class: BLACK}
424: "" {class: WHITE}
425: "" {class: BLACK}
426: "" {class: WHITE}
427: "" {class: BLACK}
428: "" {class: WHITE}
429: "" {class: BLACK}
430: "" {class: WHITE}
431: "" {class: BLACK}
432: "" {class: WHITE}
433: "" {class: BLACK}
434: "" {class: WHITE}
435: "" {class: BLACK}
436: "" {class: WHITE}
437: "" {class: BLACK}
438: "" {class: WHITE}
439: "" {class: BLACK}
440: "" {class: WHITE}
441: "" {class: BLACK}

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 86 KiB

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 86 KiB