Skip to content

Commit 692d702

Browse files
author
David Ungar
authored
Merge pull request #501 from davidungar/redo
[Incremental] Add writing dependency dot files
2 parents 5dbed2e + e9ac52e commit 692d702

14 files changed

+535
-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: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
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+
static let oneOfEachKind: [DependencyKey.Designator] = [
167+
.topLevel(name: ""),
168+
.dynamicLookup(name: ""),
169+
.externalDepend(try! ExternalDependency(".")),
170+
.sourceFileProvide(name: ""),
171+
.nominal(context: ""),
172+
.potentialMember(context: ""),
173+
.member(context: "", name: "")
174+
]
175+
}
176+
177+
// MARK: - writing one dot file
178+
179+
fileprivate struct DOTDependencyGraphSerializer<Graph: ExportableGraph> {
180+
private let includeExternals: Bool
181+
private let includeAPINotes: Bool
182+
private let graph: Graph
183+
private var nodeIDs = [Graph.Node: Int]()
184+
private var out: WritableByteStream
185+
186+
fileprivate init(
187+
_ graph: Graph,
188+
_ stream: WritableByteStream,
189+
includeExternals: Bool,
190+
includeAPINotes: Bool
191+
) {
192+
self.graph = graph
193+
self.out = stream
194+
self.includeExternals = includeExternals
195+
self.includeAPINotes = includeAPINotes
196+
}
197+
198+
fileprivate mutating func emit() {
199+
emitPrelude()
200+
emitLegend()
201+
emitNodes()
202+
emitArcs()
203+
emitPostlude()
204+
}
205+
206+
private func emitPrelude() {
207+
out <<< "digraph " <<< graph.graphID.quoted <<< " {\n"
208+
}
209+
private mutating func emitLegend() {
210+
for dummy in DependencyKey.Designator.oneOfEachKind {
211+
out <<< DotFileNode(forLegend: dummy).description <<< "\n"
212+
}
213+
}
214+
private mutating func emitNodes() {
215+
graph.forEachExportableNode { (n: Graph.Node) in
216+
if include(n) {
217+
n.emit(id: register(n), to: &out)
218+
}
219+
}
220+
}
221+
222+
private mutating func register(_ n: Graph.Node) -> Int {
223+
let newValue = nodeIDs.count
224+
let oldValue = nodeIDs.updateValue(newValue, forKey: n)
225+
assert(oldValue == nil, "not nil")
226+
return newValue
227+
}
228+
229+
private func emitArcs() {
230+
graph.forEachExportableArc { (def: Graph.Node, use: Graph.Node) in
231+
if include(def: def, use: use) {
232+
out <<< DotFileArc(defID: nodeIDs[def]!, useID: nodeIDs[use]!).description <<< "\n"
233+
}
234+
}
235+
}
236+
private func emitPostlude() {
237+
out <<< "\n}\n"
238+
}
239+
240+
func include(_ n: Graph.Node) -> Bool {
241+
let externalPredicate = includeExternals || !n.isExternal
242+
let apiPredicate = includeAPINotes || !n.isAPINotes
243+
return externalPredicate && apiPredicate;
244+
}
245+
246+
func include(def: Graph.Node, use: Graph.Node) -> Bool {
247+
include(def) && include(use)
248+
}
249+
}
250+
251+
fileprivate extension String {
252+
var quoted: String {
253+
"\"" + replacingOccurrences(of: "\"", with: "\\\"") + "\""
254+
}
255+
}
256+
257+
fileprivate struct DotFileNode: CustomStringConvertible {
258+
let id: String
259+
let label: String
260+
let shape: Shape
261+
let fillColor: Color
262+
let style: Style?
263+
264+
init<Node: ExportableNode>(id: Int, node: Node) {
265+
self.id = String(id)
266+
self.label = node.label
267+
self.shape = node.shape
268+
self.fillColor = node.fillColor
269+
self.style = node.style
270+
}
271+
272+
init(forLegend designator: DependencyKey.Designator) {
273+
self.id = designator.shape.rawValue
274+
self.label = designator.kindName
275+
self.shape = designator.shape
276+
self.fillColor = .azure
277+
self.style = nil
278+
}
279+
280+
var description: String {
281+
let bodyString: String = [
282+
("label", label),
283+
("shape", shape.rawValue),
284+
("fillcolor", fillColor.rawValue),
285+
style.map {("style", $0.rawValue)}
286+
]
287+
.compactMap {
288+
$0.map {name, value in "\(name) = \"\(value)\""}
289+
}
290+
.joined(separator: ", ")
291+
292+
return "\(id.quoted) [ \(bodyString) ]"
293+
}
294+
}
295+
296+
fileprivate struct DotFileArc: CustomStringConvertible {
297+
let defID, useID: Int
298+
299+
var description: String {
300+
"\(defID) -> \(useID);"
301+
}
302+
}
303+
304+
fileprivate enum Shape: String {
305+
case box, parallelogram, ellipse, triangle, diamond, house, hexagon
306+
}
307+
308+
fileprivate enum Color: String {
309+
case azure, white, yellow
310+
}
311+
312+
fileprivate enum Style: String {
313+
case solid, dotted
314+
}

0 commit comments

Comments
 (0)