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

SDFG Diff Tool #1632

Merged
merged 4 commits into from
Sep 14, 2024
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
220 changes: 220 additions & 0 deletions dace/cli/sdfg_diff.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
# Copyright 2019-2024 ETH Zurich and the DaCe authors. All rights reserved.
""" SDFG diff tool. """

import argparse
from hashlib import sha256
import json
import os
import platform
import tempfile
from typing import Dict, Literal, Set, Tuple, Union

import jinja2
import dace
from dace import memlet as mlt
from dace.sdfg import nodes as nd
from dace.sdfg.graph import Edge, MultiConnectorEdge
from dace.sdfg.sdfg import InterstateEdge
from dace.sdfg.state import ControlFlowBlock
import dace.serialize


DiffableT = Union[ControlFlowBlock, nd.Node, MultiConnectorEdge[mlt.Memlet], Edge[InterstateEdge]]
DiffSetsT = Tuple[Set[str], Set[str], Set[str]]


def _print_diff(sdfg_A: dace.SDFG, sdfg_B: dace.SDFG, diff_sets: DiffSetsT) -> None:
all_id_elements_A: Dict[str, DiffableT] = dict()
all_id_elements_B: Dict[str, DiffableT] = dict()

all_id_elements_A[sdfg_A.guid] = sdfg_A
for n, _ in sdfg_A.all_nodes_recursive():
all_id_elements_A[n.guid] = n
for e, _ in sdfg_A.all_edges_recursive():
all_id_elements_A[e.data.guid] = e

all_id_elements_B[sdfg_B.guid] = sdfg_B
for n, _ in sdfg_B.all_nodes_recursive():
all_id_elements_B[n.guid] = n
for e, _ in sdfg_B.all_edges_recursive():
all_id_elements_B[e.data.guid] = e

no_removed = True
no_added = True
no_changed = True
if len(diff_sets[0]) > 0:
print('Removed elements:')
for k in diff_sets[0]:
print(all_id_elements_A[k])
no_removed = False
if len(diff_sets[1]) > 0:
if not no_removed:
print('')
print('Added elements:')
for k in diff_sets[1]:
print(all_id_elements_B[k])
no_added = False
if len(diff_sets[2]) > 0:
if not no_removed or not no_added:
print('')
print('Changed elements:')
for k in diff_sets[2]:
print(all_id_elements_B[k])
no_changed = False

if no_removed and no_added and no_changed:
print('SDFGs are identical')


def _sdfg_diff(sdfg_A: dace.SDFG, sdfg_B: dace.SDFG, eq_strategy = Union[Literal['hash', '==']]) -> DiffSetsT:
all_id_elements_A: Dict[str, DiffableT] = dict()
all_id_elements_B: Dict[str, DiffableT] = dict()

all_id_elements_A[sdfg_A.guid] = sdfg_A
for n, _ in sdfg_A.all_nodes_recursive():
all_id_elements_A[n.guid] = n
for e, _ in sdfg_A.all_edges_recursive():
all_id_elements_A[e.data.guid] = e

all_id_elements_B[sdfg_B.guid] = sdfg_B
for n, _ in sdfg_B.all_nodes_recursive():
all_id_elements_B[n.guid] = n
for e, _ in sdfg_B.all_edges_recursive():
all_id_elements_B[e.data.guid] = e

a_keys = set(all_id_elements_A.keys())
b_keys = set(all_id_elements_B.keys())

added_keys = b_keys - a_keys
removed_keys = a_keys - b_keys
changed_keys = set()

remaining_keys = a_keys - removed_keys
if remaining_keys != b_keys - added_keys:
raise RuntimeError(
'The sets of remaining keys between graphs A and B after accounting for added and removed keys do not match'
)
for k in remaining_keys:
el_a = all_id_elements_A[k]
el_b = all_id_elements_B[k]

if eq_strategy == 'hash':
try:
if isinstance(el_a, Edge):
attr_a = dace.serialize.all_properties_to_json(el_a.data)
else:
attr_a = dace.serialize.all_properties_to_json(el_a)
hash_a = sha256(json.dumps(attr_a).encode('utf-8')).hexdigest()
except KeyError:
hash_a = None
try:
if isinstance(el_b, Edge):
attr_b = dace.serialize.all_properties_to_json(el_b.data)
else:
attr_b = dace.serialize.all_properties_to_json(el_b)
hash_b = sha256(json.dumps(attr_b).encode('utf-8')).hexdigest()
except KeyError:
hash_b = None

if hash_a != hash_b:
changed_keys.add(k)
else:
if isinstance(el_a, Edge):
attr_a = dace.serialize.all_properties_to_json(el_a.data)
else:
attr_a = dace.serialize.all_properties_to_json(el_a)
if isinstance(el_b, Edge):
attr_b = dace.serialize.all_properties_to_json(el_b.data)
else:
attr_b = dace.serialize.all_properties_to_json(el_b)

if attr_a != attr_b:
changed_keys.add(k)

return removed_keys, added_keys, changed_keys


def main():
# Command line options parser
parser = argparse.ArgumentParser(description='SDFG diff tool.')

# Required argument for SDFG file path
parser.add_argument('sdfg_A_path', help='<PATH TO FIRST SDFG FILE>', type=str)
parser.add_argument('sdfg_B_path', help='<PATH TO SECOND SDFG FILE>', type=str)

parser.add_argument('-g',
'--graphical',
dest='graphical',
action='store_true',
help="If set, visualize the difference graphically",
default=False)
parser.add_argument('-o',
'--output',
dest='output',
help="The output filename to generate",
type=str)
parser.add_argument('-H',
'--hash',
dest='hash',
action='store_true',
help="If set, use the hash of JSON serialized properties for change checks instead of " +
"Python's dictionary equivalence checks. This makes changes order sensitive.",
default=False)

args = parser.parse_args()

if not os.path.isfile(args.sdfg_A_path):
print('SDFG file', args.sdfg_A_path, 'not found')
exit(1)

if not os.path.isfile(args.sdfg_B_path):
print('SDFG file', args.sdfg_B_path, 'not found')
exit(1)

sdfg_A = dace.SDFG.from_file(args.sdfg_A_path)
sdfg_B = dace.SDFG.from_file(args.sdfg_B_path)

eq_strategy = 'hash' if args.hash else '=='

diff_sets = _sdfg_diff(sdfg_A, sdfg_B, eq_strategy)

if args.graphical:
basepath = os.path.join(os.path.dirname(os.path.realpath(dace.__file__)), 'viewer')
template_loader = jinja2.FileSystemLoader(searchpath=os.path.join(basepath, 'templates'))
template_env = jinja2.Environment(loader=template_loader)
template = template_env.get_template('sdfv_diff_view.html')

# if we are serving, the base path should just be root
html = template.render(sdfgA=json.dumps(dace.serialize.dumps(sdfg_A.to_json())),
sdfgB=json.dumps(dace.serialize.dumps(sdfg_B.to_json())),
removedKeysList=json.dumps(list(diff_sets[0])),
addedKeysList=json.dumps(list(diff_sets[1])),
changedKeysList=json.dumps(list(diff_sets[2])),
dir=basepath + '/')

if args.output:
fd = None
html_filename = args.output
else:
fd, html_filename = tempfile.mkstemp(suffix=".sdfg.html")

with open(html_filename, 'w') as f:
f.write(html)

if fd is not None:
os.close(fd)

system = platform.system()

if system == 'Windows':
os.system(html_filename)
elif system == 'Darwin':
os.system('open %s' % html_filename)
else:
os.system('xdg-open %s' % html_filename)
else:
_print_diff(sdfg_A, sdfg_B, diff_sets)


if __name__ == '__main__':
main()
4 changes: 2 additions & 2 deletions dace/sdfg/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ def all_nodes_recursive(self, predicate = None) -> Iterator[Tuple[NodeT, GraphT]
yield node, self
if isinstance(node, nd.NestedSDFG):
if predicate is None or predicate(node, self):
yield from node.sdfg.all_nodes_recursive()
yield from node.sdfg.all_nodes_recursive(predicate)

def all_edges_recursive(self) -> Iterator[Tuple[EdgeT, GraphT]]:
for e in self.edges():
Expand Down Expand Up @@ -966,7 +966,7 @@ def all_nodes_recursive(self, predicate = None) -> Iterator[Tuple[NodeT, GraphT]
for node in self.nodes():
yield node, self
if predicate is None or predicate(node, self):
yield from node.all_nodes_recursive()
yield from node.all_nodes_recursive(predicate)

def all_edges_recursive(self) -> Iterator[Tuple[EdgeT, GraphT]]:
for e in self.edges():
Expand Down
132 changes: 14 additions & 118 deletions dace/viewer/templates/sdfv.html
Original file line number Diff line number Diff line change
@@ -1,121 +1,17 @@
<!-- Copyright 2019-2024 ETH Zurich and the DaCe authors. All rights reserved. -->
{% extends "sdfv_base.html" %}

<!DOCTYPE html>
<html lang="en" class="sdfv">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>SDFV: SDFG Viewer</title>

<script src="{{dir|safe}}./webclient/external_lib/pdfkit.standalone.js"></script>
<script src="{{dir|safe}}./webclient/external_lib/blob-stream.js"></script>
<script src="{{dir|safe}}./webclient/external_lib/canvas2pdf.js"></script>

<script defer src="{{dir|safe}}./webclient/dist/sdfv.js"></script>
</head>

<body class="sdfv">
<div class="w3-sidebar w3-bar-block w3-card w3-animate-right"
style="display:none;right:0;" id="sidebar">
<div class="dragbar" id="dragbar"></div>
<div class="sidebar-inner">
<button id="menuclose" class="w3-bar-item w3-button w3-large">
Close &times;
</button>
<h3 id="sidebar-header">
Nothing selected
</h3>
<div id="sidebar-contents"></div>
</div>
</div>
<div class="container-fluid" id="header-container">
<div class="row g-2">
<div class="col-auto">
<input type="file" id="sdfg-file-input"
accept=".sdfg,.json,.sdfgz,.sdfg.gz"
class="form-control form-control-sm">
</div>
<div class="col-auto">
<button class="btn btn-sm btn-light btn-sdfv-light" id="reload">
Refresh
</button>
</div>
<div class="col-auto">
<button class="btn btn-sm btn-light btn-sdfv-light" id="outline">
SDFG Outline
</button>
</div>
<div class="col-auto">
<input type="file" accept=".json" id="instrumentation-report-file-input"
style="display: none;">
<button id="load-instrumentation-report-btn"
class="btn btn-sm btn-light btn-sdfv-light"
onclick="document.getElementById('instrumentation-report-file-input').click();">
Load Instrumentation Report
</button>
</div>
</div>
<div class="row g-2">
<div class="col-auto">
<div class="input-group">
<input id="search" type="text" class="form-control form-control-sm"
placeholder="Search in graph elements">
<button id="search-btn" class="btn btn-sm btn-light btn-sdfv-light">
Search
</button>
</div>
</div>
<div class="col-auto d-flex align-items-center">
<div class="form-check form-switch">
<input type="checkbox" id="search-case" class="form-check-input">
<label for="search-case" class="form-check-label">
Case Sensitive
</label>
</div>
</div>
<div class="col-auto">
<div class="dropdown">
<button class="btn btn-sm btn-light btn-sdfv-light dropdown-toggle" type="button"
data-bs-toggle="dropdown">
Advanced Search
</button>
<form class="dropdown-menu p-1">
<textarea id="advsearch" style="font-family: monospace"
class="form-control mb-2">(graph, element) => {
// Create a predicate that returns true for a match
// For example, finding transient arrays below
if (element && element.data.node) {
let arrname = element.data.node.attributes.data;
if (arrname) {
let arr = element.sdfg.attributes._arrays[arrname];
if (arr && arr.attributes.transient)
return true;
}
}
return false;
};</textarea>
<button id="advsearch-btn" class="btn btn-light btn-sdfv-light">
Search
</button>
</form>
</div>
</div>
<div class="col-auto d-flex align-items-center">
<div id="task-info-field">
</div>
</div>
</div>
</div>
<div id="contents"></div>
{% block scripts_after %}
<script>
document.addEventListener("DOMContentLoaded", function (event) {
var sdfg_json = {{sdfg|safe}};
var sdfg = parse_sdfg(sdfg_json);
init_sdfv(sdfg);
var sdfg_string = {{sdfg|safe}};
document.addEventListener('DOMContentLoaded', function () {
const sdfvInst = WebSDFV.getInstance();
if (sdfvInst.initialized) {
sdfvInst.setSDFG(checkCompatLoad(parse_sdfg(sdfg_string)), null, false);
} else {
sdfvInst.on('initialized', () => {
sdfvInst.setSDFG(checkCompatLoad(parse_sdfg(sdfg_string)), null, false);
});
}
});
</script>
</div>
</body>

</html>
</script>
{% endblock %}
Loading
Loading