Skip to content

Commit 2fd78a6

Browse files
author
David Ungar
committed
Add writing dependency dot files
1 parent 5dbed2e commit 2fd78a6

14 files changed

+533
-76
lines changed

Sources/SwiftDriver/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ add_library(SwiftDriver
4141
"IncrementalCompilation/BidirectionalMap.swift"
4242
"IncrementalCompilation/BuildRecord.swift"
4343
"IncrementalCompilation/BuildRecordInfo.swift"
44+
"IncrementalCompilation/DependencyGraphDotFileWriter.swift"
4445
"IncrementalCompilation/DependencyKey.swift"
4546
"IncrementalCompilation/DictionaryOfDictionaries.swift"
4647
"IncrementalCompilation/ExternalDependencyAndFingerprintEnforcer.swift"

Sources/SwiftDriver/IncrementalCompilation/BuildRecordInfo.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,11 @@ import SwiftOptions
252252
.parentDirectory
253253
.appending(component: filename + ".priors")
254254
}
255+
256+
/// Directory to emit dot files into
257+
var dotFileDirectory: VirtualPath {
258+
buildRecordPath.parentDirectory
259+
}
255260
}
256261

257262
fileprivate extension AbsolutePath {
Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
//===---------- DependencyGraphDotFileWriter.swift - Swift GraphViz -------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2021 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
import TSCBasic
13+
14+
// MARK: - Asking to write dot files / interface
15+
public class DependencyGraphDotFileWriter {
16+
/// Holds file-system and options
17+
private let info: IncrementalCompilationState.InitialStateComputer
18+
19+
private var versionNumber = 0
20+
21+
init(_ info: IncrementalCompilationState.InitialStateComputer) {
22+
self.info = info
23+
}
24+
25+
func write(_ sfdg: SourceFileDependencyGraph) {
26+
let basename = sfdg.dependencySource.shortDescription
27+
write(sfdg, basename: basename)
28+
}
29+
30+
func write(_ mdg: ModuleDependencyGraph) {
31+
write(mdg, basename: Self.moduleDependencyGraphBasename)
32+
}
33+
34+
public static let moduleDependencyGraphBasename = "moduleDependencyGraph"
35+
}
36+
37+
// MARK: Asking to write dot files / implementation
38+
fileprivate extension DependencyGraphDotFileWriter {
39+
func write<Graph: ExportableGraph>(_ graph: Graph, basename: String) {
40+
let path = dotFilePath(for: basename)
41+
try! info.fileSystem.writeFileContents(path) { stream in
42+
var s = DOTDependencyGraphSerializer<Graph>(
43+
graph,
44+
stream,
45+
includeExternals: info.dependencyDotFilesIncludeExternals,
46+
includeAPINotes: info.dependencyDotFilesIncludeAPINotes)
47+
s.emit()
48+
}
49+
}
50+
51+
func dotFilePath(for basename: String) -> VirtualPath {
52+
let nextVersionNumber = versionNumber
53+
versionNumber += 1
54+
return info.buildRecordInfo.dotFileDirectory
55+
.appending(component: "\(basename).\(nextVersionNumber).dot")
56+
}
57+
}
58+
59+
// MARK: - Making dependency graphs exportable
60+
fileprivate protocol ExportableGraph {
61+
var graphID: String {get}
62+
associatedtype Node: ExportableNode
63+
func forEachExportableNode(_ visit: (Node) -> Void)
64+
func forEachExportableArc(_ visit: (Node, Node) -> Void)
65+
}
66+
67+
extension SourceFileDependencyGraph: ExportableGraph {
68+
fileprivate var graphID: String {
69+
return try! VirtualPath(path: sourceFileName ?? "anonymous").basename
70+
}
71+
fileprivate func forEachExportableNode<Node: ExportableNode>(_ visit: (Node) -> Void) {
72+
forEachNode { visit($0 as! Node) }
73+
}
74+
fileprivate func forEachExportableArc<Node: ExportableNode>(_ visit: (Node, Node) -> Void) {
75+
forEachNode { use in
76+
forEachDefDependedUpon(by: use) { def in
77+
visit(def as! Node, use as! Node)
78+
}
79+
}
80+
}
81+
}
82+
83+
extension ModuleDependencyGraph: ExportableGraph {
84+
fileprivate var graphID: String {
85+
return "ModuleDependencyGraph"
86+
}
87+
fileprivate func forEachExportableNode<Node: ExportableNode>(
88+
_ visit: (Node) -> Void) {
89+
nodeFinder.forEachNode { visit($0 as! Node) }
90+
}
91+
fileprivate func forEachExportableArc<Node: ExportableNode>(
92+
_ visit: (Node, Node) -> Void
93+
) {
94+
nodeFinder.forEachNode {def in
95+
for use in nodeFinder.uses(of: def) {
96+
visit(def as! Node, use as! Node)
97+
}
98+
}
99+
}
100+
}
101+
102+
// MARK: - Making dependency graph nodes exportable
103+
fileprivate protocol ExportableNode: Hashable {
104+
var key: DependencyKey {get}
105+
var isProvides: Bool {get}
106+
var label: String {get}
107+
}
108+
109+
extension SourceFileDependencyGraph.Node: ExportableNode {
110+
}
111+
112+
extension ModuleDependencyGraph.Node: ExportableNode {
113+
fileprivate var isProvides: Bool {
114+
!isExpat
115+
}
116+
}
117+
118+
extension ExportableNode {
119+
fileprivate func emit(id: Int, to out: inout WritableByteStream) {
120+
out <<< DotFileNode(id: id, node: self).description <<< "\n"
121+
}
122+
123+
fileprivate var label: String {
124+
"\(key.description) \(isProvides ? "here" : "somewhere else")"
125+
}
126+
127+
fileprivate var isExternal: Bool {
128+
key.designator.externalDependency != nil
129+
}
130+
fileprivate var isAPINotes: Bool {
131+
key.designator.externalDependency?.file.extension == "apinotes"
132+
}
133+
134+
fileprivate var shape: Shape {
135+
key.designator.shape
136+
}
137+
fileprivate var fillColor: Color {
138+
isProvides ? .azure : key.aspect == .interface ? .yellow : .white
139+
}
140+
fileprivate var style: Style? {
141+
isProvides ? .solid : .dotted
142+
}
143+
}
144+
145+
146+
fileprivate extension DependencyKey.Designator {
147+
var shape: Shape {
148+
switch self {
149+
case .topLevel:
150+
return .box
151+
case .dynamicLookup:
152+
return .diamond
153+
case .externalDepend:
154+
return .house
155+
case .sourceFileProvide:
156+
return .hexagon
157+
case .nominal:
158+
return .parallelogram
159+
case .potentialMember:
160+
return .ellipse
161+
case .member:
162+
return .triangle
163+
}
164+
}
165+
}
166+
167+
// MARK: - writing one dot file
168+
169+
fileprivate struct DOTDependencyGraphSerializer<Graph: ExportableGraph> {
170+
private let includeExternals: Bool
171+
private let includeAPINotes: Bool
172+
private let graph: Graph
173+
private var nodeIDs = [Graph.Node: Int]()
174+
private var out: WritableByteStream
175+
176+
fileprivate init(_ graph: Graph,
177+
_ stream: WritableByteStream,
178+
includeExternals: Bool,
179+
includeAPINotes: Bool) {
180+
self.graph = graph
181+
self.out = stream
182+
self.includeExternals = includeExternals
183+
self.includeAPINotes = includeAPINotes
184+
}
185+
186+
fileprivate mutating func emit() {
187+
emitPrelude()
188+
emitLegend()
189+
emitNodes()
190+
emitArcs()
191+
emitPostlude()
192+
}
193+
194+
private func emitPrelude() {
195+
out <<< "digraph " <<< graph.graphID.quoted <<< " {\n"
196+
}
197+
private mutating func emitLegend() {
198+
for dummy in DependencyKey.Designator.oneOfEachKind {
199+
out <<< DotFileNode(forLegend: dummy).description <<< "\n"
200+
}
201+
}
202+
private mutating func emitNodes() {
203+
graph.forEachExportableNode { (n: Graph.Node) in
204+
if include(n) {
205+
n.emit(id: register(n), to: &out)
206+
}
207+
}
208+
}
209+
210+
private mutating func register(_ n: Graph.Node) -> Int {
211+
let newValue = nodeIDs.count
212+
let oldValue = nodeIDs.updateValue(newValue, forKey: n)
213+
assert(oldValue == nil, "not nil")
214+
return newValue
215+
}
216+
217+
private func emitArcs() {
218+
graph.forEachExportableArc { (def: Graph.Node, use: Graph.Node) in
219+
if include(def: def, use: use) {
220+
out <<< DotFileArc(defID: nodeIDs[def]!, useID: nodeIDs[use]!).description <<< "\n"
221+
}
222+
}
223+
}
224+
private func emitPostlude() {
225+
out <<< "\n}\n"
226+
}
227+
228+
func include(_ n: Graph.Node) -> Bool {
229+
let externalPredicate = includeExternals || !n.isExternal
230+
let apiPredicate = includeAPINotes || !n.isAPINotes
231+
return externalPredicate && apiPredicate;
232+
}
233+
234+
func include(def: Graph.Node, use: Graph.Node) -> Bool {
235+
include(def) && include(use)
236+
}
237+
}
238+
239+
fileprivate extension String {
240+
var quoted: String {
241+
"\"" + replacingOccurrences(of: "\"", with: "\\\"") + "\""
242+
}
243+
}
244+
245+
fileprivate struct DotFileNode: CustomStringConvertible {
246+
let id: String
247+
let label: String
248+
let shape: Shape
249+
let fillColor: Color
250+
let style: Style?
251+
252+
init<Node: ExportableNode>(id: Int, node: Node) {
253+
self.id = String(id)
254+
self.label = node.label
255+
self.shape = node.shape
256+
self.fillColor = node.fillColor
257+
self.style = node.style
258+
}
259+
260+
init(forLegend designator: DependencyKey.Designator) {
261+
self.id = designator.shape.rawValue
262+
self.label = designator.kindName
263+
self.shape = designator.shape
264+
self.fillColor = .azure
265+
self.style = nil
266+
}
267+
268+
var description: String {
269+
let bodyString: String = [
270+
("label", label),
271+
("shape", shape.rawValue),
272+
("fillcolor", fillColor.rawValue),
273+
style.map {("style", $0.rawValue)}
274+
]
275+
.compactMap {
276+
$0.map {name, value in "\(name) = \"\(value)\""}
277+
}
278+
.joined(separator: ", ")
279+
280+
return "\(id.quoted) [ \(bodyString) ]"
281+
}
282+
}
283+
284+
fileprivate struct DotFileArc: CustomStringConvertible {
285+
let defID, useID: Int
286+
287+
var description: String {
288+
"\(defID) -> \(useID);"
289+
}
290+
}
291+
292+
fileprivate enum Shape: String {
293+
case box, parallelogram, ellipse, triangle, diamond, house, hexagon
294+
}
295+
296+
fileprivate enum Color: String {
297+
case azure, white, yellow
298+
}
299+
300+
fileprivate enum Style: String {
301+
case solid, dotted
302+
}

0 commit comments

Comments
 (0)