[WIP] [debug-toolbar] add a rendering panel to the debug toolbar

authorLaurent Peuch <cortex@worlddomination.be>
changeset59ee6c2deee6
branchdefault
phasedraft
hiddenno
parent revision#a4d465a3e77d fix(ci): manually remove the .tox/doc directory
child revision#f316e4df5745 [debug] add mechanism to collect uicfg declarations
files modified by this revision
cubicweb/debug.py
cubicweb/pyramid/debug_toolbar_templates/rendering.dbtmako
cubicweb/pyramid/debugtoolbar_panels.py
cubicweb/pyramid/pyramidctl.py
# HG changeset patch
# User Laurent Peuch <cortex@worlddomination.be>
# Date 1567303200 -7200
# Sun Sep 01 04:00:00 2019 +0200
# Node ID 59ee6c2deee6544b296c478f3519243189202a12
# Parent a4d465a3e77d07cf6a79c121c10b2d6484cd7468
[WIP] [debug-toolbar] add a rendering panel to the debug toolbar

diff --git a/cubicweb/debug.py b/cubicweb/debug.py
@@ -21,10 +21,11 @@
1  logger = getLogger('cubicweb')
2 
3 
4  SUBSCRIBERS = {
5      "controller": [],
6 +    "rendering_classes": [],
7      "rql": [],
8      "sql": [],
9      "vreg": [],
10      "registry_decisions": [],
11  }
diff --git a/cubicweb/pyramid/debug_toolbar_templates/rendering.dbtmako b/cubicweb/pyramid/debug_toolbar_templates/rendering.dbtmako
@@ -0,0 +1,91 @@
12 +<%def name="render_tree(rc, frames_list, childs)">
13 +    <div class="tree">
14 +        % if "vid" in rc:
15 +            <span class="vid" title="__vid">"${rc["vid"]}"</span>
16 +            <span class="red">-&gt;</span>
17 +        % endif
18 +        <span class="${rc["kind"]}">${rc["object"].__class__.__name__} [${rc["kind"].title()}]</span>
19 +    </div>
20 +
21 +    <ul class="tree">
22 +        % for child_rc, child_frames_list, child_childs in childs:
23 +        <li class="tree">${render_tree(child_rc, child_frames_list, child_childs)}</li>
24 +        % endfor
25 +    </ul>
26 +</%def>
27 +
28 +<p><i>please note that this view is <b>experimental</b> and that the parent class can not always be correctly guessed</i></p>
29 +
30 +<ul class="first tree">
31 +% for rc, frames_list, childs in rendering_classes:
32 +    <li class="tree">
33 +        ${render_tree(rc, frames_list, childs)}
34 +    </li>
35 +% endfor
36 +</ul>
37 +
38 +<style>
39 +ul.tree {
40 +    padding: 0;
41 +    padding-left: 5px;
42 +    margin: 0;
43 +    list-style-type: none;
44 +    position: relative;
45 +    padding-bottom: 2px;
46 +}
47 +
48 +ul.first.tree {
49 +    padding-left: 0px;
50 +}
51 +
52 +li.tree {
53 +    list-style-type: none;
54 +    border-left: 2px solid #bc2131;
55 +    margin-left: 1.5em;
56 +    padding-bottom: 2px;
57 +}
58 +
59 +li.tree div.tree {
60 +    padding-left: 1.2em;
61 +    position: relative;
62 +    padding-top: 3px;
63 +}
64 +
65 +li.tree div.tree::before {
66 +    content:'';
67 +    position: absolute;
68 +    top: 0;
69 +    left: -2px;
70 +    bottom: 50%;
71 +    width: 1em;
72 +    border: 2px solid #bc2131;
73 +    border-top: 0 none transparent;
74 +    border-right: 0 none transparent;
75 +}
76 +
77 +ul.tree > li.tree:last-child {
78 +    border-left: 2px solid transparent;
79 +}
80 +
81 +.red {
82 +    color: #bc2131;
83 +    padding-left: 3px;
84 +    padding-right: 3px;
85 +    font-weight: bold;
86 +}
87 +
88 +.vid {
89 +    font-style: italic;
90 +    color: #7a7a7a;
91 +}
92 +
93 +.form {
94 +    color: #2b96b6;
95 +}
96 +
97 +.component {
98 +    color: #7335c0;
99 +}
100 +
101 +${generate_css() | n}
102 +</style>
diff --git a/cubicweb/pyramid/debugtoolbar_panels.py b/cubicweb/pyramid/debugtoolbar_panels.py
@@ -139,10 +139,157 @@
103 
104      def process_response(self, response):
105          unsubscribe_to_debug_channel("vreg", self.collect_vreg)
106 
107 
108 +class RenderingDebugPanel(DebugPanel):
109 +    """
110 +    CubicWeb page rendering debug panel
111 +    """
112 +
113 +    """
114 +    Excepted formats:
115 +    rendering_classes {
116 +        "kind": kind,
117 +        "object": self,
118 +        "function_name": func_name,
119 +        "arg": arg,
120 +        "frame": frame,
121 +        "function_line_number": frame.f_lineno,
122 +        "function_filename": co.co_filename,
123 +    }
124 +    """
125 +
126 +    name = 'Rendering'
127 +    has_content = True
128 +    template = 'cubicweb.pyramid:debug_toolbar_templates/rendering.dbtmako'
129 +
130 +    def __init__(self, request):
131 +        self.data = {'rendering_classes': []}
132 +        self.rendering_classes = []
133 +        subscribe_to_debug_channel("rendering_classes", self.collect_rendering_classes)
134 +
135 +    def collect_rendering_classes(self, klass):
136 +        self.rendering_classes.append(klass)
137 +
138 +    @property
139 +    def nav_title(self):
140 +        return 'Rendering'
141 +
142 +    @property
143 +    def title(self):
144 +        return 'Rendering hierarchy'
145 +
146 +    def frame_to_frames_list(self, frame):
147 +        frames_list = [frame]
148 +
149 +        while getattr(frame, "f_back", None) and frame.f_back:
150 +            frames_list.append(frame.f_back)
151 +            frame = frame.f_back
152 +
153 +        return frames_list
154 +
155 +    def rendering_tree(self, rendering_classes):
156 +        root = []
157 +        already_encountered_class = []
158 +
159 +        objects_to_childs = {}
160 +        remove_duplicates = set()
161 +
162 +        def _show_line(frame):
163 +            import re
164 +            co = frame.f_code
165 +            # because index starts at 0
166 +            func_line_no = frame.f_lineno - 1
167 +            func_filename = co.co_filename
168 +
169 +            with open(func_filename, "r") as f:
170 +                lines = f.read().split("\n")
171 +                before = "\n".join(lines[func_line_no - 3:func_line_no])
172 +
173 +                line = lines[func_line_no]
174 +                beginning = re.search("^ + ", line) and re.search("^ + ", line).group()
175 +                if beginning:
176 +                    replacement = beginning.replace(" ", "-")
177 +                    replacement = replacement[:-2] + "> "
178 +                    line = line.replace(beginning, replacement, 1)
179 +
180 +                after = "\n".join(lines[func_line_no + 1:func_line_no + 3])
181 +                return before + "\n" + line + "\n" + after
182 +
183 +        while rendering_classes:
184 +            rc = rendering_classes.pop(0)
185 +
186 +            rc_object = rc["object"]
187 +            childs = []
188 +            frames_list = self.frame_to_frames_list(rc["frame"])
189 +
190 +            if rc_object not in remove_duplicates:
191 +                remove_duplicates.add(rc_object)
192 +            else:
193 +                already_encountered_class.append((rc_object, childs))
194 +                continue
195 +
196 +            # init
197 +            if not already_encountered_class:
198 +                root.append((rc, frames_list, childs))
199 +                already_encountered_class.append((rc_object, childs))
200 +                objects_to_childs[rc_object] = childs
201 +                continue
202 +
203 +            found = False
204 +            # print()
205 +            # print(rc_object)
206 +            # print("=" * len(str(rc_object)))
207 +            for previous_rc_object, _ in reversed(already_encountered_class):
208 +                if previous_rc_object is rc_object:
209 +                    continue
210 +
211 +                for frame in frames_list:
212 +                    get_vid = [x for x in frame.f_locals.keys() if x.endswith("__vid")]
213 +                    if "vid" not in rc and get_vid:
214 +                        rc["vid"] = frame.f_locals[get_vid[0]]
215 +
216 +                    get_registry_key = [x for x in frame.f_locals.keys() if x.endswith("__registry")]
217 +                    if "registry_key" not in rc and get_registry_key:
218 +                        rc["registry_key"] = frame.f_locals[get_registry_key[0]]
219 +
220 +                    # if rc_object.__class__.__name__ == "SearchBox":
221 +                    #     print()
222 +                    #     print("%s, %s, %s, %s" % (frame, rc["function_name"], rc["arg"], frame.f_locals.get("self")))
223 +                    #     print(_show_line(frame))
224 +                    #     print()
225 +                    if frame.f_locals.get("self") is previous_rc_object:
226 +                        objects_to_childs[previous_rc_object].append((rc, frames_list, childs))
227 +                        objects_to_childs[rc_object] = childs
228 +                        already_encountered_class.append((rc_object, childs))
229 +                        found = True
230 +                        break
231 +
232 +                if found:
233 +                    break
234 +
235 +            if not found:
236 +                root.append((rc, frames_list, childs))
237 +                already_encountered_class.append((rc_object, childs))
238 +                objects_to_childs[rc_object] = childs
239 +                continue
240 +
241 +        return root
242 +
243 +    def process_response(self, response):
244 +        unsubscribe_to_debug_channel("rendering_classes", self.collect_rendering_classes)
245 +
246 +        # clear on every new response
247 +        self.data = {
248 +            'rendering_classes': self.rendering_tree(self.rendering_classes[:]),
249 +            'highlight': highlight_html,
250 +            'generate_css': generate_css,
251 +        }
252 +        self.rendering_classes = []
253 +
254 +
255  class RQLDebugPanel(DebugPanel):
256      """
257      CubicWeb RQL debug panel
258      """
259 
@@ -258,7 +405,8 @@
260 
261  def includeme(config):
262      config.add_debugtoolbar_panel(CubicWebDebugPanel)
263      config.add_debugtoolbar_panel(RegistryDecisionsDebugPanel)
264      config.add_debugtoolbar_panel(RegistryDebugPanel)
265 +    config.add_debugtoolbar_panel(RenderingDebugPanel)
266      config.add_debugtoolbar_panel(RQLDebugPanel)
267      config.add_debugtoolbar_panel(SQLDebugPanel)
diff --git a/cubicweb/pyramid/pyramidctl.py b/cubicweb/pyramid/pyramidctl.py
@@ -39,15 +39,56 @@
268  from cubicweb.pyramid.config import get_random_secret_key
269  from cubicweb.view import inject_html_generating_call_on_w
270  from cubicweb.server import serverctl
271  from cubicweb.web.webctl import WebCreateHandler
272  from cubicweb.toolsutils import fill_templated_file
273 +from cubicweb.debug import emit_to_debug_channel
274 
275 +from cubicweb import view
276 +from cubicweb.web import form, component
277  import waitress
278 
279  MAXFD = 1024
280 
281 +def debug_call_stack(frame, event, arg):
282 +    if event != 'call':
283 +        return
284 +
285 +    func_name = frame.f_code.co_name
286 +    # if func_name not in ("__init__", "__call__", "render", "render_boy"):
287 +        # return
288 +
289 +    if "self" not in frame.f_locals:
290 +        return
291 +
292 +    self = frame.f_locals["self"]
293 +
294 +    # we only care about CW situations
295 +    # and some module doesn't have a __module__
296 +    if "cubicweb" not in getattr(self, "__module__", {}):
297 +        return
298 +
299 +    if isinstance(self, component.CtxComponent):
300 +        kind = "component"
301 +    elif isinstance(self, view.View):
302 +        kind = "view"
303 +    elif isinstance(self, form.Form):
304 +        kind = "form"
305 +    else:
306 +        return
307 +
308 +    emit_to_debug_channel("rendering_classes", {
309 +        "kind": kind,
310 +        "object": self,
311 +        "function_name": func_name,
312 +        "arg": arg,
313 +        "frame": frame,
314 +        "function_line_number": frame.f_lineno,
315 +        "function_filename": frame.f_code.co_filename,
316 +    })
317 +    return
318 +
319 
320  def _generate_pyramid_ini_file(pyramid_ini_path):
321      """Write a 'pyramid.ini' file into apphome."""
322      template_fpath = os.path.join(os.path.dirname(__file__), 'pyramid.ini.tmpl')
323      context = {
@@ -286,10 +327,11 @@
324          port = cwconfig['port'] or 8080
325          url_scheme = ('https' if cwconfig['base-url'].startswith('https')
326                        else 'http')
327          repo = app.application.registry['cubicweb.repository']
328          try:
329 +            threading.settrace(debug_call_stack)
330              waitress.serve(app, host=host, port=port, url_scheme=url_scheme,
331                             clear_untrusted_proxy_headers=True, threads=self['nb-threads'])
332          finally:
333              repo.shutdown()
334          if self._needreload: