mirror of https://github.com/zulip/zulip.git
306 lines
9.9 KiB
Python
Executable File
306 lines
9.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
$ ./tools/js-dep-visualizer.py
|
|
$ dot -Tpng var/zulip-deps.dot -o var/zulip-deps.png
|
|
"""
|
|
|
|
from __future__ import absolute_import
|
|
from __future__ import print_function
|
|
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from collections import defaultdict
|
|
|
|
from typing import Any, DefaultDict, Dict, List, Set, Tuple
|
|
Edge = Tuple[str, str]
|
|
EdgeSet = Set[Edge]
|
|
Method = str
|
|
MethodDict = DefaultDict[Edge, List[Method]]
|
|
|
|
|
|
TOOLS_DIR = os.path.abspath(os.path.dirname(__file__))
|
|
ROOT_DIR = os.path.dirname(TOOLS_DIR)
|
|
sys.path.insert(0, ROOT_DIR)
|
|
from tools.lib.graph import (
|
|
Graph,
|
|
make_dot_file,
|
|
best_edge_to_remove,
|
|
)
|
|
|
|
JS_FILES_DIR = os.path.join(ROOT_DIR, 'static/js')
|
|
OUTPUT_FILE_PATH = os.path.relpath(os.path.join(ROOT_DIR, 'var/zulip-deps.dot'))
|
|
PNG_FILE_PATH = os.path.relpath(os.path.join(ROOT_DIR, 'var/zulip-deps.png'))
|
|
|
|
def get_js_edges():
|
|
# type: () -> Tuple[EdgeSet, MethodDict]
|
|
names = set()
|
|
modules = [] # type: List[Dict[str, Any]]
|
|
for js_file in os.listdir(JS_FILES_DIR):
|
|
if not js_file.endswith('.js'):
|
|
continue
|
|
name = js_file[:-3] # remove .js
|
|
path = os.path.join(JS_FILES_DIR, js_file)
|
|
names.add(name)
|
|
modules.append(dict(
|
|
name=name,
|
|
path=path,
|
|
regex=re.compile('[^_]{}\.\w+\('.format(name))
|
|
))
|
|
|
|
comment_regex = re.compile('\s+//')
|
|
call_regex = re.compile('[^_](\w+\.\w+)\(')
|
|
|
|
methods = defaultdict(list) # type: DefaultDict[Edge, List[Method]]
|
|
edges = set()
|
|
for module in modules:
|
|
parent = module['name']
|
|
|
|
with open(module['path']) as f:
|
|
for line in f:
|
|
if comment_regex.match(line):
|
|
continue
|
|
if 'subs.forEach' in line:
|
|
continue
|
|
m = call_regex.search(line)
|
|
if not m:
|
|
continue
|
|
for g in m.groups():
|
|
child, method = g.split('.')
|
|
if (child not in names):
|
|
continue
|
|
if child == parent:
|
|
continue
|
|
tup = (parent, child)
|
|
edges.add(tup)
|
|
methods[tup].append(method)
|
|
return edges, methods
|
|
|
|
def find_edges_to_remove(graph, methods):
|
|
# type: (Graph, MethodDict) -> Tuple[Graph, List[Edge]]
|
|
EXEMPT_EDGES = [
|
|
# These are sensible dependencies, so don't cut them.
|
|
('rows', 'message_store'),
|
|
('filter', 'stream_data'),
|
|
('server_events', 'user_events'),
|
|
('compose_fade', 'stream_data'),
|
|
('narrow', 'message_list'),
|
|
('stream_list', 'topic_list',),
|
|
('subs', 'stream_muting'),
|
|
('hashchange', 'settings'),
|
|
('tutorial', 'narrow'),
|
|
('activity', 'resize'),
|
|
('hashchange', 'drafts'),
|
|
('compose', 'echo'),
|
|
('compose', 'resize'),
|
|
('settings', 'resize'),
|
|
('settings', 'settings_lab'),
|
|
('settings_lab', 'resize'),
|
|
('compose', 'unread_ops'),
|
|
('compose', 'drafts'),
|
|
('echo', 'message_edit'),
|
|
('echo', 'stream_list'),
|
|
('hashchange', 'narrow'),
|
|
('hashchange', 'subs'),
|
|
('message_edit', 'echo'),
|
|
('popovers', 'message_edit'),
|
|
('unread_ui', 'activity'),
|
|
('message_fetch', 'message_util'),
|
|
('message_fetch', 'resize'),
|
|
('message_util', 'resize'),
|
|
('notifications', 'tutorial'),
|
|
('message_util', 'unread_ui'),
|
|
('muting_ui', 'stream_list'),
|
|
('muting_ui', 'unread_ui'),
|
|
('stream_popover', 'subs'),
|
|
('stream_popover', 'muting_ui'),
|
|
('narrow', 'message_fetch'),
|
|
('narrow', 'message_util'),
|
|
('narrow', 'navigate'),
|
|
('unread_ops', 'unread_ui'),
|
|
('narrow', 'unread_ops'),
|
|
('navigate', 'unread_ops'),
|
|
('pm_list', 'unread_ui'),
|
|
('stream_list', 'unread_ui'),
|
|
('popovers', 'compose'),
|
|
('popovers', 'muting_ui'),
|
|
('popovers', 'narrow'),
|
|
('popovers', 'resize'),
|
|
('pm_list', 'resize'),
|
|
('notifications', 'navigate'),
|
|
('compose', 'socket'),
|
|
('stream_muting', 'message_util'),
|
|
('subs', 'stream_list'),
|
|
('ui', 'message_fetch'),
|
|
('ui', 'unread_ops'),
|
|
('condense', 'message_viewport'),
|
|
('compose_actions', 'compose'),
|
|
('compose_actions', 'resize'),
|
|
('settings_streams', 'stream_data'),
|
|
('drafts', 'hashchange'),
|
|
('settings_notifications', 'stream_edit'),
|
|
('compose', 'stream_edit'),
|
|
('subs', 'stream_edit'),
|
|
('narrow_state', 'stream_data'),
|
|
('stream_edit', 'stream_list'),
|
|
('reactions', 'emoji_picker'),
|
|
('message_edit', 'resize'),
|
|
] # type: List[Edge]
|
|
|
|
def is_exempt(edge):
|
|
# type: (Tuple[str, str]) -> bool
|
|
parent, child = edge
|
|
if edge == ('server_events', 'reload'):
|
|
return False
|
|
if parent in ['server_events', 'user_events', 'stream_events',
|
|
'message_events', 'reload']:
|
|
return True
|
|
if child == 'rows':
|
|
return True
|
|
return edge in EXEMPT_EDGES
|
|
|
|
APPROVED_CUTS = [
|
|
('stream_edit', 'stream_events'),
|
|
('unread_ui', 'pointer'),
|
|
('typing_events', 'narrow'),
|
|
('echo', 'message_events'),
|
|
('resize', 'navigate'),
|
|
('narrow', 'search'),
|
|
('subs', 'stream_events'),
|
|
('stream_color', 'tab_bar'),
|
|
('stream_color', 'subs'),
|
|
('stream_data', 'narrow'),
|
|
('unread', 'narrow'),
|
|
('composebox_typeahead', 'compose'),
|
|
('message_list', 'message_edit'),
|
|
('message_edit', 'compose'),
|
|
('message_store', 'compose'),
|
|
('settings_notifications', 'subs'),
|
|
('settings', 'settings_muting'),
|
|
('message_fetch', 'tutorial'),
|
|
('settings', 'subs'),
|
|
('activity', 'narrow'),
|
|
('compose', 'compose_actions'),
|
|
('compose', 'subs'),
|
|
('compose_actions', 'drafts'),
|
|
('compose_actions', 'narrow'),
|
|
('compose_actions', 'unread_ops'),
|
|
('drafts', 'compose'),
|
|
('drafts', 'echo'),
|
|
('echo', 'compose'),
|
|
('echo', 'narrow'),
|
|
('echo', 'pm_list'),
|
|
('echo', 'ui'),
|
|
('message_fetch', 'activity'),
|
|
('message_fetch', 'narrow'),
|
|
('message_fetch', 'pm_list'),
|
|
('message_fetch', 'stream_list'),
|
|
('message_fetch', 'ui'),
|
|
('narrow', 'ui'),
|
|
('message_util', 'compose'),
|
|
('subs', 'compose'),
|
|
('narrow', 'hashchange'),
|
|
('subs', 'hashchange'),
|
|
('navigate', 'narrow'),
|
|
('navigate', 'stream_list'),
|
|
('pm_list', 'narrow'),
|
|
('pm_list', 'stream_popover'),
|
|
('muting_ui', 'stream_popover'),
|
|
('popovers', 'stream_popover'),
|
|
('topic_list', 'stream_popover'),
|
|
('stream_edit', 'subs'),
|
|
('topic_list', 'narrow'),
|
|
('stream_list', 'narrow'),
|
|
('stream_list', 'pm_list'),
|
|
('stream_list', 'unread_ops'),
|
|
('notifications', 'ui'),
|
|
('notifications', 'narrow'),
|
|
('notifications', 'unread_ops'),
|
|
('typing', 'narrow'),
|
|
('message_events', 'compose'),
|
|
('stream_muting', 'stream_list'),
|
|
('subs', 'narrow'),
|
|
('unread_ui', 'pm_list'),
|
|
('unread_ui', 'stream_list'),
|
|
('overlays', 'hashchange'),
|
|
('emoji_picker', 'reactions'),
|
|
]
|
|
|
|
def cut_is_legal(edge):
|
|
# type: (Edge) -> bool
|
|
parent, child = edge
|
|
if child in ['reload', 'popovers', 'overlays', 'notifications',
|
|
'server_events', 'compose_actions']:
|
|
return True
|
|
return edge in APPROVED_CUTS
|
|
|
|
graph.remove_exterior_nodes()
|
|
removed_edges = list()
|
|
print()
|
|
while graph.num_edges() > 0:
|
|
edge = best_edge_to_remove(graph, is_exempt)
|
|
if edge is None:
|
|
print('we may not be allowing edge cuts!!!')
|
|
break
|
|
if cut_is_legal(edge):
|
|
graph = graph.minus_edge(edge)
|
|
graph.remove_exterior_nodes()
|
|
removed_edges.append(edge)
|
|
else:
|
|
for removed_edge in removed_edges:
|
|
print(removed_edge)
|
|
print()
|
|
edge_str = str(edge) + ','
|
|
print(edge_str)
|
|
for method in methods[edge]:
|
|
print(' ' + method)
|
|
break
|
|
|
|
return graph, removed_edges
|
|
|
|
def report_roadmap(edges, methods):
|
|
# type: (List[Edge], MethodDict) -> None
|
|
child_modules = {child for parent, child in edges}
|
|
module_methods = defaultdict(set) # type: DefaultDict[str, Set[str]]
|
|
callers = defaultdict(set) # type: DefaultDict[Tuple[str, str], Set[str]]
|
|
for parent, child in edges:
|
|
for method in methods[(parent, child)]:
|
|
module_methods[child].add(method)
|
|
callers[(child, method)].add(parent)
|
|
|
|
for child in sorted(child_modules):
|
|
print(child + '.js')
|
|
for method in module_methods[child]:
|
|
print(' ' + child + '.' + method)
|
|
for caller in sorted(callers[(child, method)]):
|
|
print(' ' + caller + '.js')
|
|
print()
|
|
print()
|
|
|
|
def produce_partial_output(graph):
|
|
# type: (Graph) -> None
|
|
print(graph.num_edges())
|
|
buffer = make_dot_file(graph)
|
|
|
|
graph.report()
|
|
with open(OUTPUT_FILE_PATH, 'w') as f:
|
|
f.write(buffer)
|
|
subprocess.check_call(["dot", "-Tpng", OUTPUT_FILE_PATH, "-o", PNG_FILE_PATH])
|
|
print()
|
|
print('See dot file here: {}'.format(OUTPUT_FILE_PATH))
|
|
print('See output png file: {}'.format(PNG_FILE_PATH))
|
|
|
|
def run():
|
|
# type: () -> None
|
|
edges, methods = get_js_edges()
|
|
graph = Graph(edges)
|
|
graph, removed_edges = find_edges_to_remove(graph, methods)
|
|
if graph.num_edges() == 0:
|
|
report_roadmap(removed_edges, methods)
|
|
else:
|
|
produce_partial_output(graph)
|
|
|
|
if __name__ == '__main__':
|
|
run()
|