]> Witch of Git - web/blog/blob - posts/2018/debugging-the-deep-end.md
Update some dates to add timezones
[web/blog] / posts / 2018 / debugging-the-deep-end.md
1 ---
2 title: "Debugging in the Deep End"
3 date: 2018-04-17 America/Detroit
4 tags:
5 - code
6 - open source
7 author: Cassie Jones
8 summary: "I discuss the approach I used to fix a bug in VCV Rack despite having never looked at the codebase before."
9 ---
10
11 # The Problem
12
13 Last week I was working with [Aaron][] on a series of [VCV Rack plugin modules][VCVMicroTools], and we were trying to add our own custom graphics for them.
14 VCV Rack uses SVG for its plugins, so Aaron had built a front face for one of our modules, but it wasn't properly aligned.
15 I imported it into Affinity Designer and tried to fix it up, but when I exported my new version and loaded it, suddenly all of our modules had vanished.
16 Since our module wasn't *supposed* to vanish, and I hadn't done anything *obviously* wrong, I decided that this must be a bug in VCV Rack.
17 Over the next few hours, I diagnosed and managed to fix this bug, and by the magic of open source and some luck, the PRs got merged the next day.
18 In particular, I managed to make this fix without having ever looked at any of this code before, and I'd like to share the process I followed to manage to do this.
19
20 [Aaron]: http://twitter.com/a2aarontothe2
21 [VCVMicroTools]: https://github.com/a2aaron/VCVMicroTools
22 <!-- Use these links? -->
23 [VCV Issue]: https://github.com/VCVRack/Rack/issues/917
24 [nanosvg PR]: https://github.com/memononen/nanosvg/pull/116
25
26 # Debugging the SVG
27
28 The first phase when fixing a bug is to reproduce the bug.
29 Here, because the rendering worked fine with Aaron's SVG until I re-exported it, I suspected that some feature being used in Affinity's SVG export wasn't supported by the VCV Rack SVG renderer.
30 To figure out which, I used the first technique: minimize your failing case.
31
32 First, I tried changing export settings, removing groups to flatten the SVG, doing everything I could to remove different features.
33 As I went, I inspected the working and not working SVG side-by-side to see what the differences were.
34 I didn't make much progress this way, so I started from the other direction, building up instead of tearing down.
35 I saved a simple blank grey square, just a single element.
36 When that didn't work, I figured it must have something to do with one of the attributes on the `<svg>` container element.
37 For reference, a minimal SVG exported from Affinity might look something like:
38
39 ```svg
40 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
41 <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
42 "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
43 <svg width="100%" height="100%" viewBox="0 0 240 380" version="1.1"
44 xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
45 xml:space="preserve" xmlns:serif="http://www.serif.com/"
46 style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;
47 stroke-miterlimit:1.41421;">
48 <rect x="0" y="0" width="240" height="380" style="fill:rgb(235,235,235);"/>
49 </svg>
50 ```
51
52 So I looked through all the settings on that, I noticed that it was setting the width and height to `100%`, whereas the working one was setting it to explicit pixel numbers.
53 I copied the width and height out of the working one into the not-working one… and that fixed it.
54 That suggested a possible problem: If you used percentage dimensions on the `<svg>` element, it wouldn't correctly calculate the size of the object, and would simply make it 0 by 0.
55 This was a good enough guess for me, so I set about trying to figure out how to fix that.
56
57 # Spelunking the Code
58
59 This brings us to the second phase of solving the bug: find a piece of code that's related to the bug, so you have a place to start.
60 I suspected that I could find where the SVGs were loaded in VCV Rack and fix it to handle those percentages correctly.
61 I didn't know exactly how I would handle them yet, I had to see what it was doing first.
62 To find this, I took a simple approach: search the source code for the word "SVG" and see what I could find!
63 I used [ripgrep][], a very good search tool, but you can use whatever tool you have available as long as it can search all the code at once.
64 If your editor can jump to definitions in a project, searching for related words and then jumping from definition to definition can help you find the part of the code you're interested in very quickly; having good code navigation tools helps *a lot*.
65
66 [ripgrep]: https://github.com/BurntSushi/ripgrep
67
68 Using this, I found SVG widgets, followed their class hierarchy up to rendering components, and then eventually I found my way to a class calling functions from "nanosvg."
69 Curious, I looked it up, and saw that it was a small SVG parser library, and that it produces a bunch of shape paths.
70 In order to not have to resize all those paths (I assumed), I decided to try fixing the bug from inside nanosvg instead of inside VCV Rack.
71 Knowing that it was a problem with dimensions, I searched the nanosvg code for the string `"width"`.
72 The second result was a very promising looking function:
73
74 ```c hl_lines="6 7 8 9"
75 static void nsvg__parseSVG(NSVGparser* p, const char** attr)
76 {
77 int i;
78 for (i = 0; attr[i]; i += 2) {
79 if (!nsvg__parseAttr(p, attr[i], attr[i + 1])) {
80 if (strcmp(attr[i], "width") == 0) {
81 p->image->width = nsvg__parseCoordinate(p, attr[i + 1], 0.0f, 0.0f);
82 } else if (strcmp(attr[i], "height") == 0) {
83 p->image->height = nsvg__parseCoordinate(p, attr[i + 1], 0.0f, 0.0f);
84 // …
85 ```
86
87 # Writing a Fix
88
89 I'd located a likely location for the bug, so now I changed mode from code spelunking to trying to understand what the code did.
90 Since this function looked so relevant, I first tried to figure out what `nsvg__parseSVG` was doing.
91 A good tool for this was finding where it was used: it was getting called in one place, from `nsvg__startElement`, and seemed to be being called when an `<svg>` tag was found, to compute the context from the attributes… perfect.
92 The parameter `const char** attr` suggested a list of attribute strings, and the usage `attr[i]` and `attr[i + 1]` suggested the SVG key/value pairs.
93 Therefore, it seemed like
94
95 ```c
96 if (strcmp(attr[i], "width") == 0)
97 p->image->width = nsvg__parseCoordinate(p, attr[i + 1], 0.0f, 1.0f);
98 ```
99
100 would parse the width coordinate value.
101 In order to figure this out, we want to go look at `nsvg__parseCoordinate`.
102
103 ```c
104 static float nsvg__parseCoordinate(NSVGparser* p, const char* str,
105 float orig, float length)
106 {
107 NSVGcoordinate coord = nsvg__parseCoordinateRaw(str);
108 return nsvg__convertToPixels(p, coord, orig, length);
109 }
110 ```
111
112 Following those definitions, `nsvg__parseCoordinateRaw` follows a few steps to get to unit parsing, but it seems largely straightforward parsing of the data, no fancy processing.
113 The fact that we've got an issue in % suggests that `nsvg__convertToPixels` is doing something interesting.
114 And indeed, looking at the code for that function, it made clear what the `length` argument did:
115
116 ```c hl_lines="7"
117 static float nsvg__convertToPixels(NSVGparser* p, NSVGcoordinate c,
118 float orig, float length)
119 {
120 NSVGattrib* attr = nsvg__getAttr(p);
121 switch (c.units) {
122 // …
123 case NSVG_UNITS_PERCENT: return orig + c.value / 100.0f * length;
124 default: return c.value;
125 }
126 return c.value;
127 }
128 ```
129
130 It was used as the base value that the percentage should be relative to.
131 Then, it becomes clear: `nsvg__parseCoordinate(p, attr[i + 1], 0.0f, 1.0f);` makes `100%` into `1px`
132 So, now we know what exactly has gone wrong, how do we solve it?
133 Since I didn't know what the percentages should be relative to, I started researching, looking at Mozilla references for how the percent should behave.
134
135 I didn't find an answer, but while I was researching, I ran into lots of examples that didn't specify dimensions at all.
136 This made me suspicious: nanosvg handles most SVGs correctly, so it must have some code to handle this case.
137 When you're fixing a bug, often the edge case that you're running into is similar to another edge case that's already handled, and you just need to make it cover your case as well.
138 Since this must be related to the dimensions, and the dimension handling sets the `width` field while parsing the `<svg>` element, I went out searching for `->width` and `.width` in the code.
139 I immediately found `nsvg__scaleToViewbox` which contains a promising looking block of code:
140
141 ```c hl_lines="1"
142 if (p->viewWidth == 0) {
143 if (p->image->width > 0) {
144 p->viewWidth = p->image->width;
145 } else {
146 p->viewMinx = bounds[0];
147 p->viewWidth = bounds[2] - bounds[0];
148 }
149 }
150 ```
151
152 This looks like what we want!
153 It will recalculate the width and height if they're set to 0, so we just need to make sure that our 100% sets it to 0 instead of 1.
154 And to fix that, we can simply change:
155
156 ```diff
157 if (strcmp(attr[i], "width") == 0) {
158 - p->image->width = nsvg__parseCoordinate(p, attr[i + 1], 0.0f, 1.0f);
159 + p->image->width = nsvg__parseCoordinate(p, attr[i + 1], 0.0f, 0.0f);
160 } else if (strcmp(attr[i], "height") == 0) {
161 - p->image->height = nsvg__parseCoordinate(p, attr[i + 1], 0.0f, 1.0f);
162 + p->image->height = nsvg__parseCoordinate(p, attr[i + 1], 0.0f, 0.0f);
163 } else if (strcmp(attr[i], "viewBox") == 0) {
164 ```
165
166 And that's the whole fix!
167
168 # Conclusions
169
170 You can use these techniques the next time you have to jump into a large codebase that's unfamiliar.
171 Finding a simple case that fails, making a hypothesis about why it fails, and then searching for terms related to that gives you a big head-start navigating the code.
172 Being able to jump to definitions helps you build a mental map of a thin slice of the code.
173 Even though Rack is about 11K lines of code, and nanosvg is almost 3K, in the process of fixing this bug I only *glanced at* a few hundred lines of code, and only tried to understand a few dozen of them.
174 The next time you want to try to examine a new codebase, keep these tricks in mind.