[Developer,Blog,Blog,Post] Updates to the Git Commit Graph Feature
diff mbox series

Message ID 20191021204253.10196-1-dstolee@microsoft.com
State New
Headers show
Series
  • [Developer,Blog,Blog,Post] Updates to the Git Commit Graph Feature
Related show

Commit Message

Derrick Stolee Oct. 21, 2019, 8:42 p.m. UTC
In this blog post, we discuss updates in to the Git commit-graph
feature since it was announced shortly after Git 2.18.0. This
answers the following:

1. What is the commit-graph?
2. Why should I enable the commit-graph?
3. How do I enable it now, or disable in time for 2.24.0?
4. How do I write it during fetch?
5. What is a commit-graph chain?

I intended the post to start at a level that any experienced Git
user could understand, but then it ramps up in detail by the end
to include some very deep technical details. I expect most readers
to give up after the first couple sections, but those who stick
to the end will learn something valuable.

Signed-off-by: Derrick Stolee <dstolee@microsoft.com>
---

Inspired by Emily's first contribution in the space [1], I now finished this
post that I've been working on here-and-there since Git 2.23.0. Now that 2.24.0
is imminent, the post is even more relevant. This is available as a Merge
Request [2] on the git-blog repo. You should be able to see the markdown style
and the SVG better there.

To create the image, I used Inkscape, then SVGMinify.com to reduce the file
size, which hopefully makes this patch less obnoxious.

[1] https://public-inbox.org/git/20191019002045.148579-1-emilyshaffer@google.com/

[2] https://gitlab.com/git-scm/blog/merge_requests/5/diffs#241fcd5d0d81901ba224af0d603905f593aaf821

 .../post/2019-10-31-commit-graph-updates.md   | 268 ++++++++++++++++++
 content/post/img/2019-commit-graph-chain.svg  |  85 ++++++
 2 files changed, 353 insertions(+)
 create mode 100644 content/post/2019-10-31-commit-graph-updates.md
 create mode 100644 content/post/img/2019-commit-graph-chain.svg

Patch
diff mbox series

diff --git a/content/post/2019-10-31-commit-graph-updates.md b/content/post/2019-10-31-commit-graph-updates.md
new file mode 100644
index 0000000..abf6a14
--- /dev/null
+++ b/content/post/2019-10-31-commit-graph-updates.md
@@ -0,0 +1,268 @@ 
+---
+title: Updates to the Git Commit Graph Feature
+author: Derrick Stolee, Microsoft
+date: '2019-10-31'
+draft: true
+categories:
+  - Feature Announcement
+tags:
+  - commit-graph
+  - performance
+  - features
+---
+
+In [a previous blog series](https://devblogs.microsoft.com/devops/supercharging-the-git-commit-graph/),
+we announced that Git has a new _commit graph_ feature, and described some
+future directions. Since then, the commit-graph feature has grown and evolved.
+In the upcoming Git version 2.24.0, [the commit-graph will be enabled by default!](https://github.com/git/git/commit/31b1de6a09bad59cc0d88419925486afc7add277#diff-ec15845924b3ae854680823745518271)
+Today, we discuss what you should know about the feature, and what you can
+expect when you upgrade.
+
+# What is the commit-graph, and what is it good for?
+
+The commit-graph file is a binary file format that creates a structured
+representation of Git's commit history. At minimum, the commit-graph file
+is [faster to parse](https://github.com/git/git/blob/master/commit-graph.c#L606-L668)
+than decompressing commit files and [parsing them](https://github.com/git/git/blob/master/commit.c#L395-L455)
+to find their parents and root trees. This faster parsing can lead to 10x
+performance improvements.
+
+To get even more performance benefits, Git does not just use the commit-graph
+file to parse commits faster, but the commit-graph includes extra information
+to help avoid parsing some commits altogether. The critical idea is that an
+extra value -- the [generation number](https://devblogs.microsoft.com/devops/supercharging-the-git-commit-graph-iii-generations/)
+of a commit -- can significantly reduce the number of commits we need to walk
+to decide reachability. Since Git 2.19.0, the commit-graph
+[stores generation numbers](https://github.com/git/git/commit/3258c66332abaf6e3e8fd81cab07ae804760cd08).
+
+Since then, multiple algorithms were introduced to speed up Git commands such as
+[force push](https://github.com/git/git/commit/1e3497a24cf13fe907b247d1b93a997d6537cca1) or
+[fetch negotiation](https://github.com/git/git/commit/4fbcca4effc1c6f8431120f88f5a4bd1c8e38ca3).
+
+Finally, the most immediately visible improvement is the time it takes to sort
+commits by topological order. This algorithm is the critical path for `git log
+--graph`. Before the commit-graph, Git needed to walk every reachable commit
+before returning a single result.
+
+For example, here is a run of `git log --graph` in the Linux repository without
+the commit-graph feature, timing how long it takes to return ten results:
+
+```
+$ time git -c core.commitGraph=false log --graph --oneline -10 >/dev/null
+
+real    0m6.103s
+user    0m5.803s
+sys     0m0.300s
+```
+
+The reason it takes so long is because [Kahn's algorithm](https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm)
+computes the "in-degrees" of every reachable commit before it can start to
+select commits of in-degree zero for output. When the commit-graph is present
+with generation numbers, Git now [uses an iterative version of Kahn's algorithm](https://github.com/git/git/commit/b45424181e9e8b2284a48c6db7b8db635bbfccc8)
+to avoid walking too far before knowing that some of the commits have in-degree
+zero and can be sent to output.
+
+Here is that same command again, this time with the commit-graph feature enabled:
+
+```
+$ time git -c core.commitGraph=true log --graph --oneline -10 >/dev/null
+
+real    0m0.009s
+user    0m0.000s
+sys     0m0.008s
+```
+
+Six seconds to nine milliseconds is a 650x speedup! Since most users asking
+for `git log --graph` actually see the result in a paged terminal window,
+this allows Git to load the first page of results almost instantaneously, and
+the next pages are available as you scroll through the history.
+
+The incremental nature of the algorithm can really be seen in real-time by
+using `git log --graph -- <file>` to investigate file history. When asking for
+a file that is not edited frequently, Git will still need to walk many commits
+to return the few results. For example, the Linux kernel `README` was edited
+in 46 of the 870,000+ commits of the repo. Running `git log --graph -- README`
+takes 0.33 seconds, but even in that time you can see the commits filling the
+page one-by-one. Without the commit-graph, the command pauses while it computes,
+then all results fill the page at the same time.
+
+# Sounds Great! What do I need to do?
+
+If you are using Git 2.23.0 or later, then you have all of these benefits
+available to you! You just need to enable the following config settings:
+
+* `git config --global core.commitGraph true`: this enables every Git repo to
+  use the commit-graph file, if present.
+
+* `git config --global gc.writeCommitGraph true`: this setting tells the `git gc`
+  command to write the commit-graph file whenever you do non-trivial garbage
+  collection. Rewriting the commit-graph file is a relatively small operation
+  compared to a full GC operation.
+
+* `git commit-graph write --reachable`: this command will update your
+  commit-graph file to contain all reachable commits. You can run this to create
+  the commit-graph file immediately, instead of waiting for your first GC operation.
+
+In the upcoming Git version 2.24.0, `core.commitGraph` and `gc.writeCommitGraph` 
+are [on by default](https://github.com/git/git/commit/31b1de6a09bad59cc0d88419925486afc7add277),
+so you don't need to set the config manually. If you _don't_ want commit-graph
+files, then explicitly disable these settings.
+
+# Write during fetch
+
+The point of the `gc.writeCommitGraph` is to keep your commit-graph updated
+with some frequency. As you add commits to your repo, the commit-graph gets
+further and further behind. That means your commit walks will parse more
+commits the old-fashioned way until finally reaching the commits in the
+commit-graph file.
+
+When working in a Git repo with many collaborators, the primary source of
+commits is not your own `git commit` calls, but your `git fetch` calls.
+However, if your repo is large enough, writing the commit-graph after each
+fetch can actually make a significant impact on your performance. Perhaps
+you downloaded a thousand new commits, but your repo has a million total
+commits. Writing the full commit-graph operates on the size of your repo,
+not on the size of your fetch, so writing those million commits is costly.
+
+During garbage collection, you are already paying for a full repack of all
+of your Git objects. That operation is already on the scale of your entire
+repository, so adding a full commit-graph write on top of that is not
+a problem.
+
+There is a solution: don't write the whole commit-graph every time! We'll
+go into how this works in more detail in the next section, but first you
+can enable the [`fetch.writeCommitGraph`](https://github.com/git/git/commit/50f26bd035816c2bb79582b834d59b49292502a9#diff-ec15845924b3ae854680823745518271)
+config setting to write the commit-graph after every `git fetch` command:
+
+```
+git config --global fetch.writeCommitGraph true
+```
+
+This ensures that your commit-graph is updated frequently and your Git
+commands are always as fast as possible.
+
+# Incremental Commit-Graph Format
+
+Before getting too far into the inremental file format, we need to refresh some
+details about the commit-graph file itself.
+
+## A Single Commit-Graph File
+
+The [commit-graph file format](https://github.com/git/git/blob/master/Documentation/technical/commit-graph-format.txt)
+stores commit data in a set of tables.
+
+One table is a sorted list of commit IDs. This row number of a commit ID in this
+table defines the _lexicographical position_ -- _lex_ position for short -- of
+a commit in the file.
+
+Another table contains metadata about the commits. The _n_th row of the metadata
+table corresponds to the commit with lex position _n_. This table contains the
+root tree ID, commit time, generation number, and information on the first two
+parents of the commit. We use special constants to say "this commit does not
+have a second parent", and use a pointer to a third "extra edges" table in the
+case of octopus merges.
+
+The two parent columns are stored as integers, and this is very important! If
+we store parents as commit IDs, then we waste a lot of space. Further, if we
+only have a commit ID, then we need to perform a binary search on the commit
+list to find the lex position. By storing the position of a parent, we can
+navigate to the metadata row for that parent as a random-access lookup.
+
+For that reason, the commit-graph file is _closed under reachability_, meaning
+that if a commit is in the file, then so is its parent. Otherwise, we could not
+refer to the parent using an integer.
+
+Before incremental writes, Git stored the commit-graph file as
+`.git/objects/info/commit-graph`. Git looks for that file, and parses the data
+there if it exists.
+
+## Multiple Commit-Graph Files
+
+If the single `.git/objects/info/commit-graph` file does not exist, Git looks
+for a file called `.git/objects/info/commit-graphs/commit-graph-chain`. This file
+contains a list of SHA-1 hashes separated by newlines. To demonstrate, we
+will use this list of placeholders:
+
+```
+{hash0}
+{hash1}
+{hash2}
+```
+
+These hashes correspond to files named
+`.git/objects/info/commit-graphs/graph-{hash0}.graph`. The chain of the three
+files combine to describe a set of commits.
+
+The first graph file, `graph-{hash0}.graph`, is a normal commit-graph file. It
+does not refer to any other commit-graph and is closed under reachability.
+
+The second graph file, `graph-{hash1}.graph` is no longer a normal commit-graph
+file. To start, it contains a pointer to `graph-{hash0}.graph` by storing an
+extra "base graphs" table containing only "{hash0}". Second, the parents of the
+commits in `graph-{hash1}.graph` may exist in that file _or_ in `graph-{hash0}.graph`.
+Each graph file stores the commits in lexicographic order, but we now need a
+second term for the position of a commit in the combined order.
+
+We say the _graph position_ of a commit in the commit-graph chain is the lex
+position of a commit in the sorted list plus the number of commits in the base
+commmit-graph files. We now modify our definition of a parent position to use
+the graph position. This allows the `graph-{hash1}.graph` file to not be closed
+under reachability: the parents can exist in either file.
+
+Extending to `graph-{hash2}.graph`, the parents of those commits can be in any
+of the three commit-graph files. The figure below shows this stack of files and
+how one commit row in `graph-{hash2}.graph` can have parents in `graph-{hash1}.graph`
+and `graph-{hash0}.graph`.
+
+![A chain of three commit-graphs](img/2019-commit-graph-chain.svg)
+
+To create your own commit-graph chain, you can start with an existing commit-graph
+file (created by `git commit-graph write --reachable`, for instance) then create
+new commits and run `git commit-graph write --reachable --split`. The `--split`
+option enables creating a chain of commit-graph files. If you ever run the command
+without the `--split` option, then the chain will merge into a single file.
+
+If you enable `fetch.writeCommitGraph`, then Git will write a commit-graph
+chain after every `git fetch` command. This is much faster than rewriting the
+entire file, since the top layer of the chain can consist of only the new
+commits. At least, it will _usually_ be faster.
+
+The figure above hints at the sizes of the commit-graph files in a chain. The
+base file is large and contains most of the commits. As we look higher in the
+chain, the sizes should shrink.
+
+There is a problem, though. What happens when we fetch 100 times? Will we get
+a chain of 100 commit-graph files? Will our commit lookups suddenly get much
+slower? The way to avoid this is to occasionally merge layers of the chain.
+This results in better [amortized time](https://en.wikipedia.org/wiki/Amortized_analysis),
+but will sometimes result in a full rewrite of the entire commit-graph file.
+
+## Merging Commit-Graph Files
+
+To ensure that the commit-graph chain does not get too long, Git will occasionally
+merge layers of the chain. This merge operation is always due to some number of
+incoming commits causing the "top" of the chain to be too large. There are two
+reasons Git would merge layers, given by these options to `git commit-graph
+write`:
+
+1. `--size-multiple=<X>`: Ensure that a commit-graph file is `X` times larger
+   than any commit-graph file "above" it. `X` defaults to 2.
+
+2. `--max-commits=<M>`: When specified, make sure that only the base layer
+   has more than `M` commits.
+
+The size-multiple option ensures that the commit-graph chain never has more
+than log(_N_) layers, where _N_ is total number of commits in the repo. If those
+chains seem to be too long, the max-commits setting (in conjunction with size-multiple)
+guarantees that there are a constant number of possible layers.
+
+In all, you should not see the incremental commit-graph taking very long during
+a fetch. You are more likely to see the automatic garbage collection trigger, and
+that will cause your commit-graph chain to collapse to a single layer.
+
+# Try it Yourself!
+
+We would love your feedback on the feature! Please test out the `--split` option
+for writing commit-graphs in Git 2.23.0 or later, and the `fetch.writeCommitGraph`
+option in Git 2.24.0. Release candidates for Git 2.24.0 are out now, and ready
+for testing!
diff --git a/content/post/img/2019-commit-graph-chain.svg b/content/post/img/2019-commit-graph-chain.svg
new file mode 100644
index 0000000..b426853
--- /dev/null
+++ b/content/post/img/2019-commit-graph-chain.svg
@@ -0,0 +1,85 @@ 
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="5.6894in" height="5.2459in" version="1.1" viewBox="0 0 144.51104 133.24566" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+<defs>
+<marker id="b" overflow="visible" orient="auto">
+<path transform="matrix(-.4 0 0 -.4 -4 0)" d="m0 0 5-5-17.5 5 17.5 5z" fill-rule="evenodd" stroke="#000" stroke-width="1pt"/>
+</marker>
+<marker id="g" overflow="visible" orient="auto">
+<path transform="matrix(-.4 0 0 -.4 -4 0)" d="m0 0 5-5-17.5 5 17.5 5z" fill-rule="evenodd" stroke="#000" stroke-width="1pt"/>
+</marker>
+<marker id="c" overflow="visible" orient="auto">
+<path transform="scale(.8)" d="m0 5.65v-11.3" fill="none" stroke="#000" stroke-width="1pt"/>
+</marker>
+<marker id="d" overflow="visible" orient="auto">
+<path transform="scale(.8)" d="m0 5.65v-11.3" fill="none" stroke="#000" stroke-width="1pt"/>
+</marker>
+<marker id="e" overflow="visible" orient="auto">
+<path transform="scale(.8)" d="m0 5.65v-11.3" fill="none" stroke="#000" stroke-width="1pt"/>
+</marker>
+<marker id="f" overflow="visible" orient="auto">
+<path transform="scale(.8)" d="m0 5.65v-11.3" fill="none" stroke="#000" stroke-width="1pt"/>
+</marker>
+<marker id="a" overflow="visible" orient="auto">
+<path transform="scale(.8)" d="m0 5.65v-11.3" fill="none" stroke="#000" stroke-width="1pt"/>
+</marker>
+</defs>
+<metadata>
+<rdf:RDF>
+<cc:Work rdf:about="">
+<dc:format>image/svg+xml</dc:format>
+<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
+<dc:title/>
+</cc:Work>
+</rdf:RDF>
+</metadata>
+<g transform="translate(4.2812 -168.05)">
+<rect x="-4.21" y="168.04" width="144.45" height="133.58" fill="#fff" stroke="#fff" stroke-width=".31055"/>
+<path d="m121.93 180.78h5.6241c1.662 0 3 1.338 3 3v28.341c0 1.662-1.338 3-3 3h-3.5074" fill="none" marker-end="url(#g)" stroke="#000" stroke-width=".3"/>
+<path d="m121.93 180.78h8.7991c1.662 0 3 1.338 3 3v62.208c0 1.662-1.338 3-3 3h-6.6824" fill="none" marker-end="url(#b)" stroke="#000" stroke-width=".3"/>
+<path d="m27.217 173.08h94.37c0.554 0 1 0.446 1 1v13.612c0 0.554-0.446 1-1 1h-94.37c-0.554 0-1-0.446-1-1v-13.612c0-0.554 0.446-1 1-1z" fill="#f2f2f2" stroke="#000"/>
+<path d="m27.217 196.28h94.37c0.554 0 1 0.446 1 1v29.487c0 0.554-0.446 1-1 1h-94.37c-0.554 0-1-0.446-1-1v-29.487c0-0.554 0.446-1 1-1z" fill="#f2f2f2" stroke="#000"/>
+<path d="m27.217 235.44h94.37c0.554 0 1 0.446 1 1v61.766c0 0.554-0.446 1-1 1h-94.37c-0.554 0-1-0.446-1-1v-61.766c0-0.554 0.446-1 1-1z" fill="#f2f2f2" stroke="#000"/>
+<path d="m26.414 178.35h95.909" fill="none" stroke="#000" stroke-width=".26458px"/>
+<path d="m26.414 183.64h95.909" fill="none" stroke="#000" stroke-width=".26458px"/>
+<path d="m26.414 201.63h95.909" fill="none" stroke="#000" stroke-width=".26458px"/>
+<path d="m26.414 206.93h95.909" fill="none" stroke="#000" stroke-width=".26458px"/>
+<path d="m26.414 212.22h95.909" fill="none" stroke="#000" stroke-width=".26458px"/>
+<path d="m26.414 217.51h95.909" fill="none" stroke="#000" stroke-width=".26458px"/>
+<path d="m26.414 222.8h95.909" fill="none" stroke="#000" stroke-width=".26458px"/>
+<path d="m26.414 240.79h95.909" fill="none" stroke="#000" stroke-width=".26458px"/>
+<path d="m26.414 246.08h95.909" fill="none" stroke="#000" stroke-width=".26458px"/>
+<path d="m26.414 251.38h95.909" fill="none" stroke="#000" stroke-width=".26458px"/>
+<path d="m26.414 256.67h95.909" fill="none" stroke="#000" stroke-width=".26458px"/>
+<path d="m26.414 261.96h95.909" fill="none" stroke="#000" stroke-width=".26458px"/>
+<path d="m26.414 267.25h95.909" fill="none" stroke="#000" stroke-width=".26458px"/>
+<path d="m26.414 272.54h95.909" fill="none" stroke="#000" stroke-width=".26458px"/>
+<path d="m26.414 277.83h95.909" fill="none" stroke="#000" stroke-width=".26458px"/>
+<path d="m26.414 283.13h95.909" fill="none" stroke="#000" stroke-width=".26458px"/>
+<path d="m26.414 288.42h95.909" fill="none" stroke="#000" stroke-width=".26458px"/>
+<path d="m26.414 293.71h95.909" fill="none" stroke="#000" stroke-width=".26458px"/>
+<path d="m21.281 188.13v-9.7292" fill="none" marker-end="url(#a)" marker-start="url(#a)" stroke="#000" stroke-width=".265"/>
+<path d="m8.0517 299.26v-120.85" fill="none" marker-end="url(#e)" marker-start="url(#f)" stroke="#000" stroke-width=".265"/>
+<path d="m21.281 299.26v-103.39" fill="none" marker-end="url(#c)" marker-start="url(#d)" stroke="#000" stroke-width=".265"/>
+<text transform="rotate(-90)" x="-230.33467" y="6.1092191" fill="#000000" font-family="Arial" font-size="7.0556px" letter-spacing="0px" stroke-width=".26458" text-align="center" text-anchor="middle" word-spacing="0px" style="line-height:1.25" xml:space="preserve"><tspan x="-230.33467" y="6.1092191" font-size="5.6444px" stroke-width=".26458">graph position</tspan></text>
+<text transform="rotate(-90)" x="-239.95189" y="19.898293" fill="#000000" font-family="Arial" font-size="7.0556px" letter-spacing="0px" stroke-width=".26458" text-align="center" text-anchor="middle" word-spacing="0px" style="line-height:1.25" xml:space="preserve"><tspan x="-239.95189" y="19.898293" font-size="5.6444px" stroke-width=".26458">number of base commits</tspan></text>
+<text transform="rotate(-90)" x="-178.97917" y="18.012175" fill="#000000" font-family="Arial" font-size="7.0556px" letter-spacing="0px" stroke-width=".26458" text-align="center" text-anchor="middle" word-spacing="0px" style="line-height:1.25" xml:space="preserve"><tspan x="-178.97917" y="18.012175" font-size="5.6444px" stroke-width=".26458" style="line-height:0">position</tspan></text>
+<text transform="rotate(-90)" x="-184.98222" y="13.381416" fill="#000000" font-family="Arial" font-size="7.0556px" letter-spacing="0px" stroke-width=".26458" text-align="center" text-anchor="middle" word-spacing="0px" style="line-height:1.25" xml:space="preserve"><tspan x="-184.98222" y="13.381416" font-size="5.6444px" stroke-width=".26458" style="line-height:0">lex</tspan></text>
+<rect x="26.414" y="178.35" width="95.909" height="5.2917" fill="#fca" stroke="#000" stroke-width=".3026"/>
+<text x="40.151272" y="182.74898" fill="#000000" font-family="Arial" font-size="4.9389px" letter-spacing="0px" stroke-width=".26458" text-align="center" text-anchor="middle" word-spacing="0px" style="line-height:1.25" xml:space="preserve"><tspan x="40.151272" y="182.74898" font-size="4.9389px" stroke-width=".26458" style="line-height:0">commit id</tspan></text>
+<text x="69.255447" y="182.74898" fill="#000000" font-family="Arial" font-size="4.9389px" letter-spacing="0px" stroke-width=".26458" text-align="center" text-anchor="middle" word-spacing="0px" style="line-height:1.25" xml:space="preserve"><tspan x="69.255447" y="182.74898" font-size="4.9389px" stroke-width=".26458" style="line-height:0">tree id</tspan></text>
+<text x="101.99261" y="182.67987" fill="#000000" font-family="Arial" font-size="4.9389px" letter-spacing="0px" stroke-width=".26458" text-align="center" text-anchor="middle" word-spacing="0px" style="line-height:1.25" xml:space="preserve"><tspan x="101.99261" y="182.67987" font-size="4.9389px" stroke-width=".26458" style="line-height:0">parent positions</tspan></text>
+<path d="m56.847 173.34v15.348" fill="none" stroke="#000" stroke-width=".26458px"/>
+<path d="m81.718 173.34v15.348" fill="none" stroke="#000" stroke-width=".26458px"/>
+<path d="m56.847 196.62v30.694" fill="none" stroke="#000" stroke-width=".26458px"/>
+<path d="m81.718 196.62v30.694" fill="none" stroke="#000" stroke-width=".26458px"/>
+<path d="m56.847 235.78v63.502" fill="none" stroke="#000" stroke-width=".26458px"/>
+<path d="m81.718 235.78v63.502" fill="none" stroke="#000" stroke-width=".26458px"/>
+<path d="m103.41 173.34v15.348" fill="none" stroke="#000" stroke-dasharray="2.1199999, 2.1199999" stroke-width=".265"/>
+<path d="m103.41 196.62v31.223" fill="none" stroke="#000" stroke-dasharray="2.1199999, 2.1199999" stroke-width=".265"/>
+<path d="m103.41 235.78v62.973" fill="none" stroke="#000" stroke-dasharray="2.1199999, 2.1199999" stroke-width=".265"/>
+<text x="44.008171" y="171.36497" fill="#000000" font-family="Arial" font-size="4.4781px" letter-spacing="0px" stroke-width=".16793" text-align="center" text-anchor="middle" word-spacing="0px" style="line-height:1.25" xml:space="preserve"><tspan x="44.008171" y="171.36497" font-size="3.5825px" stroke-width=".16793" style="line-height:0">graph-{hash2}.graph</tspan></text>
+<text x="44.008171" y="171.36497" fill="#000000" font-family="Arial" font-size="4.4781px" letter-spacing="0px" stroke-width=".16793" text-align="center" text-anchor="middle" word-spacing="0px" style="line-height:1.25" xml:space="preserve"><tspan x="44.008171" y="171.36497" font-size="3.5825px" stroke-width=".16793" style="line-height:0">graph-{hash2}.graph</tspan></text>
+<text x="44.008171" y="195.17746" fill="#000000" font-family="Arial" font-size="4.4781px" letter-spacing="0px" stroke-width=".16793" text-align="center" text-anchor="middle" word-spacing="0px" style="line-height:1.25" xml:space="preserve"><tspan x="44.008171" y="195.17746" font-size="3.5825px" stroke-width=".16793" style="line-height:0">graph-{hash1}.graph</tspan></text>
+<text x="44.008171" y="234.3358" fill="#000000" font-family="Arial" font-size="4.4781px" letter-spacing="0px" stroke-width=".16793" text-align="center" text-anchor="middle" word-spacing="0px" style="line-height:1.25" xml:space="preserve"><tspan x="44.008171" y="234.3358" font-size="3.5825px" stroke-width=".16793" style="line-height:0">graph-{hash0}.graph</tspan></text>
+</g>
+</svg>