Skip to content

Commit bc887cd

Browse files
Performance improvement refactors
1 parent 8d37a7c commit bc887cd

15 files changed

+137
-67
lines changed

.vscode/launch.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
"pathToBundler": "C:/tools/ruby25/bin/bundle.bat",
2525
"pathToRDebugIDE": "C:/tools/ruby25/bin/rdebug-ide.bat",
2626
"showDebuggerOutput": true,
27-
"args": ["generate_graph", "--root_dir", "../protobuf/src", "--output_file", "deps.dot"]
27+
"args": ["visualise", "--root_dir", "../protobuf/src"]
2828
},
2929
{
3030
"name": "Listen for rdebug-ide",

TODO.md

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
- [ ] Work with any type of include relative includes ('blah.h') vs absolute includes ('/path/blah.h') vs relative with path includes ('blah/blah.h')
1616
- [ ] Look at https://github.com/Sarcasm/compdb to see if it can generate dependencies
1717
- [ ] Parallelise dependency scanning as much as possible to get the best possible performance
18+
- [ ] Use a threadpool? Look at reference implementations
1819
- [ ] Open up the graph automatically after generating an individual graph
1920
- [ ] Work with any sort of project structure
2021
- [ ] Header only projects

exe/cpp_dependency_graph

+5-5
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,11 @@
2525
require 'docopt'
2626
require 'cpp_dependency_graph'
2727

28-
# require 'ruby-prof'
28+
require 'ruby-prof'
2929

3030
include CppDependencyGraph
3131

32-
# RubyProf.start
32+
RubyProf.start
3333

3434
doc = <<DOCOPT
3535
Cpp Dependency Graph
@@ -82,6 +82,6 @@ rescue Docopt::Exit => e
8282
puts e.message
8383
end
8484

85-
# result = RubyProf.stop
86-
# printer = RubyProf::CallTreePrinter.new(result)
87-
# printer.print(path: '.', profile: 'cpp_dependency_graph')
85+
result = RubyProf.stop
86+
printer = RubyProf::CallTreePrinter.new(result)
87+
printer.print(path: '.', profile: 'cpp_dependency_graph')

lib/cpp_dependency_graph/cycle_detector.rb

+5-7
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,11 @@
55
# Detects cycles between components
66
class CycleDetector
77
def initialize(component_links)
8-
@cyclic_links = {}
9-
component_links.each do |source, links|
10-
links.each do |target|
11-
links_of_target = component_links[target]
12-
@cyclic_links[CyclicLink.new(source, target)] = true if links_of_target.include?(source)
8+
@cyclic_links = component_links.flat_map do |source, links|
9+
links.select { |target| component_links[target].include?(source) }.map do |target|
10+
[CyclicLink.new(source, target), true]
1311
end
14-
end
12+
end.to_h
1513
end
1614

1715
def cyclic?(source, target)
@@ -20,6 +18,6 @@ def cyclic?(source, target)
2018
end
2119

2220
def to_s
23-
@cyclic_links.each { |k, _| puts k }
21+
@cyclic_links.keys
2422
end
2523
end

lib/cpp_dependency_graph/dependency_graph.rb

+11-9
Original file line numberDiff line numberDiff line change
@@ -24,24 +24,26 @@ def all_cyclic_links
2424

2525
def component_links(name)
2626
return {} unless all_component_links.key?(name)
27-
incoming_links(name).merge(outgoing_links(name))
27+
links = incoming_links(name)
28+
links.merge!(outgoing_links(name))
29+
links
2830
end
2931

3032
private
3133

3234
def build_hash_component_links
33-
raw_links = @project.source_components.map { |c| [c.name, @project.dependencies(c)] }.to_h
34-
cycle_detector = CycleDetector.new(raw_links)
35+
raw_links = @project.source_components.values.map { |c| [c.name, @project.dependencies(c)] }.to_h
36+
@cycle_detector ||= CycleDetector.new(raw_links)
3537
component_links = raw_links.map do |source, links|
36-
c_links = links.map { |target| ComponentLink.new(source, target, cycle_detector.cyclic?(source, target)) }
38+
c_links = links.map { |target| ComponentLink.new(source, target, @cycle_detector.cyclic?(source, target)) }
3739
[source, c_links]
3840
end.to_h
3941
component_links
4042
end
4143

4244
def build_cyclic_links
43-
cyclic_links = all_component_links.select { |_, links| links.any? { |link| link.cyclic? } }
44-
cyclic_links.each { |_, links| links.select! { |link| link.cyclic? } }
45+
cyclic_links = all_component_links.select { |_, links| links.any?(&:cyclic?) }
46+
cyclic_links.each_value { |links| links.select! { |link| link.cyclic? } }
4547
end
4648

4749
def outgoing_links(name)
@@ -50,9 +52,9 @@ def outgoing_links(name)
5052

5153
def incoming_links(target)
5254
incoming_c_links = all_component_links.select { |c, c_links| c_links.any? { |link| link.target == target } }
53-
incoming_c_links.map do |c_name, _|
54-
link = ComponentLink.new(c_name, target, false)
55-
[c_name, [link]]
55+
incoming_c_links.map do |source, _|
56+
link = ComponentLink.new(source, target, @cycle_detector.cyclic?(source, target))
57+
[source, [link]]
5658
end.to_h
5759
end
5860
end

lib/cpp_dependency_graph/graph_visualiser.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ def generate_dot_file(deps, file)
88
@g = Graphviz::Graph.new(name = 'dependency_graph')
99

1010
node_names = deps.flat_map do |_, links|
11-
links.map { |link| [link.source, link.target] }.flatten
11+
links.map { |link| [link.source, link.target] }
1212
end.uniq
1313
nodes = node_names.map { |name| [name, create_node(name)] }.to_h
1414

@@ -25,7 +25,7 @@ def generate_dot_file(deps, file)
2525

2626
def generate_html_file(deps, file)
2727
node_names = deps.flat_map do |_, links|
28-
links.map { |link| [link.source, link.target] }.flatten
28+
links.map { |link| [link.source, link.target] }
2929
end.uniq
3030
nodes = node_names.map { |name| { name: name } }
3131

lib/cpp_dependency_graph/include_dependency_graph.rb

+5-4
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@ def initialize(project)
1313
def include_links(component_name)
1414
component = @project.source_component(component_name)
1515
source_files = component.source_files
16+
external_includes = @project.external_includes(component)
1617
source_files.map do |file|
17-
class_name = file.basename
1818
# TODO: Very inefficient
19-
internal_includes = file.includes.reject { |inc| @project.external_includes(component).any?(inc) }
20-
include_links = internal_includes.map { |inc| ComponentLink.new(class_name, inc, false) }
21-
[class_name, include_links]
19+
source = file.basename
20+
internal_includes = file.includes.reject { |inc| external_includes.any?(inc) }
21+
include_links = internal_includes.map { |inc| ComponentLink.new(source, inc, false) }
22+
[source, include_links]
2223
end.to_h
2324
end
2425
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# frozen_string_literal: true
2+
3+
require_relative 'source_component'
4+
5+
# Resolves a given include to a source component
6+
class IncludeToComponentResolver
7+
def initialize(components)
8+
@components = components
9+
@component_external_include_cache = {}
10+
@component_include_map_cache = {}
11+
end
12+
13+
def external_includes(component)
14+
unless @component_external_include_cache.key?(component)
15+
@component_external_include_cache[component] = external_includes_private(component)
16+
end
17+
@component_external_include_cache[component]
18+
end
19+
20+
def component_for_include(include)
21+
return '' unless source_files.key?(include)
22+
unless @component_include_map_cache.key?(include)
23+
@component_include_map_cache[include] = component_for_include_private(include)
24+
end
25+
@component_include_map_cache[include]
26+
end
27+
28+
private
29+
30+
def external_includes_private(component)
31+
include_components = component.includes.map { |inc| [inc, component_for_include(inc)] }.to_h
32+
external_include_components = include_components.delete_if { |_, c| c == component.name }
33+
external_include_components.keys
34+
end
35+
36+
def component_for_include_private(include)
37+
header_file = source_files[include]
38+
implementation_files = implementation_files(header_file)
39+
return header_file.parent_component if implementation_files.empty?
40+
implementation_files[0].parent_component
41+
end
42+
43+
def implementation_files(header_file)
44+
implementation_files = []
45+
source_files.each_value do |file|
46+
implementation_files.push(file) if file.basename_no_extension == header_file.basename_no_extension
47+
end
48+
implementation_files.reject! { |file| file.basename == header_file.basename }
49+
end
50+
51+
def source_files
52+
@source_files ||= build_source_file_map
53+
end
54+
55+
def build_source_file_map
56+
# TODO SourceComponent should return a hash for source files which can be merged here
57+
source_files = @components.values.flat_map(&:source_files)
58+
source_files.map { |s| [s.basename, s] }.to_h
59+
end
60+
end

lib/cpp_dependency_graph/project.rb

+12-30
Original file line numberDiff line numberDiff line change
@@ -2,62 +2,44 @@
22

33
require 'find'
44

5+
require_relative 'include_to_component_resolver'
56
require_relative 'source_component'
67

78
# Parses all components of a project
89
class Project
910
def initialize(path)
1011
@path = path
12+
@include_resolver = IncludeToComponentResolver.new(source_components)
1113
end
1214

1315
def source_components
1416
@source_components ||= build_source_components
1517
end
1618

1719
def source_component(name)
18-
source_components.detect { |c| c.name == name }
20+
return SourceComponent.new('NULL') unless source_components.key?(name)
21+
source_components[name]
1922
end
2023

2124
def dependencies(component)
22-
external_includes(component).map { |include| component_for_include(include) }.reject(&:empty?).uniq
25+
# TODO: This is repeating the same work twice! component_for_include is called when calling external_includes
26+
external_includes(component).map { |include| @include_resolver.component_for_include(include) }.reject(&:empty?).uniq
2327
end
2428

2529
def external_includes(component)
26-
filter_internal_includes(component)
30+
@include_resolver.external_includes(component)
2731
end
2832

2933
private
3034

3135
def build_source_components
3236
# TODO: Dealing with source components with same dir name?
3337
dirs = fetch_all_dirs(@path)
34-
source_components = dirs.map { |dir| SourceComponent.new(dir) }
35-
source_components.reject { |c| c.source_files.size.zero? }
36-
end
37-
38-
def filter_internal_includes(component)
39-
# TODO: This is super inefficient, refactor it
40-
source_file_basenames = component.source_files.map(&:basename)
41-
include_components = component.includes.map { |inc| [inc, component_for_include(inc)] }.to_h
42-
filter = include_components.reject { |_, c| c == component.name }
43-
filter.keys
44-
end
45-
46-
def component_for_include(include)
47-
header_file = source_files.find { |file| file.basename == include }
48-
parent_component(header_file)
49-
end
50-
51-
def source_files
52-
@source_files ||= source_components.flat_map(&:source_files)
53-
end
54-
55-
def parent_component(header_file)
56-
return '' if header_file.nil?
57-
files = source_files.select { |file| file.basename_no_extension == header_file.basename_no_extension }
58-
corresponding_files = files.reject { |file| file.basename == header_file.basename }
59-
return header_file.parent_component if corresponding_files.size == 0
60-
corresponding_files[0].parent_component
38+
components = dirs.map do |dir|
39+
c = SourceComponent.new(dir)
40+
[c.name, c]
41+
end.to_h
42+
components.delete_if { |k, v| v.source_files.size.zero? }
6143
end
6244

6345
def fetch_all_dirs(source_dir)

lib/cpp_dependency_graph/source_component.rb

+1-2
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ def loc
3131

3232
def parse_source_files(extensions)
3333
path = File.join(@path, File::SEPARATOR) + '*' + extensions
34-
files = Dir.glob(path).select { |e| File.file?(e) }
35-
files.map { |file| SourceFile.new(file) }
34+
Dir.glob(path).map { |e| SourceFile.new(e) if File.file?(e) }.compact
3635
end
3736
end

lib/cpp_dependency_graph/source_file.rb

+3-2
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,9 @@ def all_includes
4141
end
4242

4343
def scan_includes
44-
# file_contents.scan(/#include "([^"]+)"/).uniq.flatten
45-
file_contents.scan(/#include ["|<](.+)["|>]/).uniq.flatten # TODO: use compiler lib to scan includes? llvm/clang?
44+
includes = file_contents.scan(/#include ["|<](.+)["|>]/) # TODO: use compiler lib to scan includes? llvm/clang?
45+
includes.uniq!
46+
includes.flatten
4647
end
4748

4849
def file_contents

lib/cpp_dependency_graph/version.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# frozen_string_literal: true
22

33
module CppDependencyGraph
4-
VERSION = '0.1.1'
4+
VERSION = '0.1.2'
55
end

spec/test/dependency_graph_spec.rb

+5-1
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,13 @@
1919
expect(dependency_graph.all_component_links).to eq(expected_links)
2020
end
2121

22+
it 'returns empty links for an unknown component of a project' do
23+
expect(dependency_graph.component_links('Blah').empty?).to eq(true)
24+
end
25+
2226
it 'returns links for a specified component of a project' do
2327
expected_links = {}
24-
expected_links['UI'] = [ComponentLink.new('UI', 'Engine', false)] # TODO
28+
expected_links['UI'] = [ComponentLink.new('UI', 'Engine', true)]
2529
expected_links['Engine'] = [ComponentLink.new('Engine', 'Framework', false),
2630
ComponentLink.new('Engine', 'UI', true),
2731
ComponentLink.new('Engine', 'DataAccess', false)]
+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
require 'cpp_dependency_graph/include_dependency_graph'
2+
require 'cpp_dependency_graph/component_link'
3+
4+
RSpec.describe IncludeDependencyGraph do
5+
let(:project) { Project.new('spec/test/example_project') }
6+
let(:include_dependency_graph) { IncludeDependencyGraph.new(project) }
7+
8+
it 'returns empty hash for an unknown component' do
9+
expect(include_dependency_graph.include_links('Unknown').empty?).to eq(true)
10+
end
11+
12+
it 'returns include links for a specified component' do
13+
expected_links = {}
14+
expected_links['OldEngine.h'] = []
15+
expected_links['Engine.h'] = [ComponentLink.new('Engine.h', 'Framework/framework.h', false),
16+
ComponentLink.new('Engine.h', 'UI/Display.h', false)]
17+
expected_links['Engine.cpp'] = [ComponentLink.new('Engine.cpp', 'DataAccess/DA.h', false),
18+
ComponentLink.new('Engine.cpp', 'Engine.h', false)]
19+
expect(include_dependency_graph.include_links('Engine')).to eq(expected_links)
20+
end
21+
end

spec/test/project_spec.rb

+4-3
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22

33
RSpec.describe Project do
44
it 'parses all source components' do
5-
component_names = Project.new('spec/test/example_project').source_components.map(&:name).sort
5+
component_names = Project.new('spec/test/example_project').source_components.values.map(&:name).sort
66
expect(component_names).to eq(['DataAccess', 'Engine', 'Framework', 'System', 'UI', 'main'])
77
end
88

9-
it 'returns nil component if case does not match' do
9+
it 'returns null component if case does not match' do
1010
component = Project.new('spec/test/example_project').source_component('enGine')
11-
expect(component).to eq(nil)
11+
expect(component.name).to eq('NULL')
12+
expect(component.source_files.size).to eq(0)
1213
end
1314

1415
it 'returns dependencies of component' do

0 commit comments

Comments
 (0)