Suppose a monkey is in one corner of a room, a box is in another corner of a room, and a banana is hanging from the ceiling in the middle of the room. The monkey can’t reach the banana without standing on the box, but he first has to move the box under the banana. The problem of how to get a computer to work out that the monkey has to move the box first, then climb on the box second, was solved by Nils Nilsson’s STRIPS system in 1971. STRIPS is now an A.I. standard, and is used in game A.I. and elsewhere.
Suppose you have a disk image template that you want to uncompress, convert to another format, and resize. You can run xzcat
, followed by qemu-img convert
followed by virt-resize. But virt-resize
can also do format conversion, so you don’t need to run qemu-img convert
. Unless the user was happy with the original size, in which case qemu-img convert
is faster than virt-resize. But what if the original template isn’t compressed and is already in the right format and size? You can just run cp
.
How can a computer work out the right sequence of steps to convert the disk image most efficiently? Virt-builder has exactly this problem, and it solves it using a STRIPS-inspired planner.
The STRIPS planner in virt-builder is only 50 lines of code, was easy to write, finds the near optimal plan for almost any user input, and is a useful technique that can be applied to many programming problems. This article will explain how it works. I have changed some of the academic terms and simplified things to make this easier to understand.
First of all I’ll introduce tags
on the original template. These define the state of that template:
Input tags: ✚xz ✚template ✚size=4G ✚format=raw
Secondly I’ll set up my goal state:
Goal tags: ❌xz ❌template ✚size=4G ✚format=qcow2
where ❌ means the tag MUST NOT exist in the final state.
I want my planner to find me the best path from my input state to my goal. As it can’t go straight from the input to the goal in one step, I have to tell the planner what transitions
are possible, using a function:
transitions (input_tags) { if ✚xz then { you could run 'xzcat' which will ❌xz and ❌template; } else /* no xz tag */ { you could run 'virt-resize' which will change ✚format and ✚size, and ❌template; or: you could run 'qemu-img convert' which will change ✚format, and ❌template; or: etc... } or: you could run 'cp' which will ❌template; }
Notice that the transitions function returns a list of all possible transitions from the input state. It’s not judgemental about which one should be taken, although it won’t return impossible transitions (for example, running virt-resize
is not possible on xz-compressed files). The actual transitions function also returns a weight for each transition, so that the planner can choose the least expensive plan if there are several plans possible.
The ✚template tag may appear a bit mysterious. It’s there to make sure that the planner always copies the original template, even if the original template already has the desired goal size and format. Since xzcat
, virt-resize
and qemu-img convert
always copy the disk image, they drop the template tag (❌template).
The transitions function in virt-builder can be found here.
The planner does a breadth-first search over the tree of transitions, starting with the input state, finishing when it finds any branch that satisfies the output goals, or when it reaches a maximum depth in which case it gives up (and the user sees an error message).
The planner in virt-builder (50 lines of code) can be found here.
If the planner finds several paths that satisfy the goals, the planner chooses the one with the smallest weight. However my planner is not clever enough to look deeper in the tree to see if a longer path might have a smaller weight (it’s not very likely in virt-builder).
Also my planner is not smart enough to prune bogus paths. For example, if a path runs cp
in adjacent steps, then that path should be pruned.
Nevertheless the planner always gets the right result, and it is considerably simpler than the original hand-written code. The old code had become unmaintainable and wasn’t even efficient: it sometimes made unnecessary copies in order to make the code simpler, wasting end-user time. Because of the ease of maintenance I was able to add new functionality: virt-builder can now run qemu-img resize
to expand a disk by < 256 MB, a case where virt-resize doesn’t work (previously the user would have got an error).
Applying old academic techniques like this one doesn’t need to be hard and can help with real world problems. I hope this technique helps others with similar optimization problems.
Edit: The Hacker News discussion includes links to alternative solving tools.