Merge lp:~ben-hutchings/ensoft-sextant/filter-search into lp:ensoft-sextant

Proposed by Ben Hutchings
Status: Superseded
Proposed branch: lp:~ben-hutchings/ensoft-sextant/filter-search
Merge into: lp:ensoft-sextant
Prerequisite: lp:~ben-hutchings/ensoft-sextant/autocomplete-fix
Diff against target: 607 lines (+196/-94)
7 files modified
resources/sextant/web/interface.html (+2/-2)
src/sextant/__main__.py (+9/-1)
src/sextant/db_api.py (+78/-46)
src/sextant/objdump_parser.py (+79/-33)
src/sextant/test_parser.py (+1/-1)
src/sextant/update_db.py (+15/-8)
src/sextant/web/server.py (+12/-3)
To merge this branch: bzr merge lp:~ben-hutchings/ensoft-sextant/filter-search
Reviewer Review Type Date Requested Status
Robert Pending
Review via email: mp+241967@code.launchpad.net

This proposal has been superseded by a proposal from 2014-11-18.

Commit message

Function name search within the web frontend now supports extended syntax:
'<name matches>:<file path matches>'
where name matches and file path matches are (possibly) comma separated lists, and may include wildcards '.*'. At least one of the two must be specified.

Fixed bug with inline functions being uploaded multiple times into the database.

Description of the change

Function name search within the web frontend now supports extended syntax:
'<name matches>:<file path matches>'
where name matches and file path matches are (possibly) comma separated lists, and may include wildcards '.*'. At least one of the two must be specified.

Fixed bug with inline functions being uploaded multiple times into the database.

To post a comment you must log in.
43. By Ben Hutchings

bug from lstrip removing unwanted characters

44. By Ben Hutchings

merge to fix function stubs being added incorrectly

45. By Ben Hutchings

markup fixes

46. By Ben Hutchings

markups + small bug fixes - tests do not pass (though the functionality works).

47. By Ben Hutchings

tuple instead of list

48. By Ben Hutchings

merge from autocomplete-fix

49. By Ben Hutchings

another merge from autocomplete-fix

50. By Ben Hutchings

fixed bug causing extrac characters to be removed from the start of symbol names

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'resources/sextant/web/interface.html'
2--- resources/sextant/web/interface.html 2014-11-18 14:47:39 +0000
3+++ resources/sextant/web/interface.html 2014-11-18 14:47:39 +0000
4@@ -27,8 +27,8 @@
5 All functions calling specific function</option>
6 <option value="functions_called_by">
7 All functions called by a specific function</option>
8- <option value="all_call_paths">
9- All function call paths between two functions</option>
10+ <!--option value="all_call_paths"> REMOVED AS THIS IS SLOW FOR IOS
11+ All function call paths between two functions</option-->
12 <option value="shortest_call_path">
13 Shortest path between two functions</option>
14 <option value="function_names">
15
16=== modified file 'src/sextant/__main__.py'
17--- src/sextant/__main__.py 2014-10-17 15:30:14 +0000
18+++ src/sextant/__main__.py 2014-11-18 14:47:39 +0000
19@@ -128,6 +128,7 @@
20 alternative_name = None
21
22 not_object_file = args.not_object_file
23+ add_file_paths = args.add_file_paths
24 # the default is "yes, this is an object file" if not-object-file was
25 # unsupplied
26
27@@ -136,7 +137,8 @@
28 getpass.getuser(),
29 args.input_file,
30 alternative_name,
31- not_object_file)
32+ not_object_file,
33+ add_file_paths)
34 except requests.exceptions.ConnectionError as e:
35 msg = 'Connection error to server {}: {}'
36 logging.error(msg.format(_displayable_url(args), e))
37@@ -221,6 +223,12 @@
38 help='default False, if the input file is an '
39 'object to be disassembled',
40 action='store_true')
41+ parsers['add'].add_argument('--add-file-paths',
42+ help='default False, set to True to make objdump '
43+ 'extract the file paths for each function. '
44+ 'WARNING: this is SLOW for large object files, '
45+ '~15 hours for IOS.',
46+ action='store_true')
47
48 parsers['delete'] = subparsers.add_parser('delete-program',
49 help="delete a program from the database")
50
51=== modified file 'src/sextant/db_api.py'
52--- src/sextant/db_api.py 2014-11-18 14:47:39 +0000
53+++ src/sextant/db_api.py 2014-11-18 14:47:39 +0000
54@@ -159,7 +159,7 @@
55 tmp_path = os.path.join(self._tmp_dir, '{}_{{}}'.format(program_name))
56
57 self.func_writer = CSVWriter(tmp_path.format('funcs'),
58- headers=['name', 'type'],
59+ headers=['name', 'type', 'file'],
60 max_rows=5000)
61 self.call_writer = CSVWriter(tmp_path.format('calls'),
62 headers=['caller', 'callee'],
63@@ -171,7 +171,7 @@
64 ' WITH line, toInt(line.id) as lineid'
65 ' MATCH (n:program {{name: "{}"}})'
66 ' CREATE (n)-[:subject]->(m:func {{name: line.name,'
67- ' id: lineid, type: line.type}})')
68+ ' id: lineid, type: line.type, file: line.file}})')
69
70 self.add_call_query = (' USING PERIODIC COMMIT 250'
71 ' LOAD CSV WITH HEADERS FROM "file:{}" AS line'
72@@ -203,7 +203,7 @@
73 # Propagate the error if there is one.
74 return False if etype is not None else True
75
76- def add_function(self, name, typ='normal'):
77+ def add_function(self, name, typ='normal', source='unknown'):
78 """
79 Add a function.
80
81@@ -219,7 +219,7 @@
82 pointer: we know only that the function exists, not its
83 name or details.
84 """
85- self.func_writer.write(name, typ)
86+ self.func_writer.write(name, typ, source)
87
88 def add_call(self, caller, callee):
89 """
90@@ -264,7 +264,11 @@
91 for path in file_paths:
92 os.remove(path)
93
94- os.rmdir(self._tmp_dir)
95+ try:
96+ os.rmdir(self._tmp_dir)
97+ except:
98+ # There is other stuff there - leave it.
99+ pass
100
101 try:
102 # If the parent sextant temp folder is empty, remove it.
103@@ -290,6 +294,7 @@
104
105 tx.append('CREATE CONSTRAINT ON (p:program) ASSERT p.name IS UNIQUE')
106 tx.append('CREATE INDEX ON :func(name)')
107+ tx.append('CREATE INDEX ON: func(file)')
108
109 # Apply the transaction.
110 tx.commit()
111@@ -854,6 +859,48 @@
112 .format(program_name, search, max_funcs))
113 return {func[0] for func in self._db.query(q)}
114
115+ @staticmethod
116+ def get_query(identifier, search, typ='func'):
117+ if ':' in search:
118+ func_subs, file_subs = search.split(':')
119+ else:
120+ func_subs, file_subs = search, ''
121+
122+ # Remove empty strings.
123+ func_subs = [sub for sub in func_subs.split(',') if sub]
124+ file_subs = [sub for sub in file_subs.split(',') if sub]
125+
126+ # Cases:
127+ # specific:<don't care>
128+ # wild: specific
129+ # wild: wild
130+
131+ query_str = ""
132+
133+ def get_list(subs):
134+ return '[{}]'.format(','.join("'{}'".format(s) for s in subs))
135+
136+
137+ if func_subs and not any('*' in sub for sub in func_subs):
138+ # List of specific functions. Don't care about anything after ':'
139+ query_str += ('USING INDEX {0}:func(name) WHERE {0}.name IN {1} '
140+ .format(identifier, get_list(func_subs)))
141+ else:
142+ if file_subs and not any('*' in sub for sub in file_subs):
143+ # Specific file to look in.
144+ query_str = ('USING INDEX {0}.func(file) WHERE {0}.file IN {1} '
145+ .format(identifier, get_list(file_subs)))
146+ elif file_subs:
147+ query_str = ('WHERE ANY (s_file IN {1} WHERE {0}.file =~ s_file) '
148+ .format(identifier, get_list(file_subs)))
149+
150+ if func_subs:
151+ query_str += 'AND ' if file_subs else 'WHERE '
152+ query_str += ('ANY (s_name IN {1} WHERE {0}.name =~ s_name) '
153+ .format(identifier, get_list(func_subs)))
154+
155+ return query_str
156+
157 def get_all_functions_called(self, program_name, function_calling):
158 """
159 Execute query to find all functions called by a function (indirectly).
160@@ -863,14 +910,9 @@
161 :param function_calling: string name of a function whose children to find
162 :return: FunctionQueryResult, maximal subgraph rooted at function_calling
163 """
164-
165- if not self.check_function_exists(program_name, function_calling):
166- return None
167-
168- q = (' MATCH (p:program {{name: "{}"}})-[:subject]->(f:func {{name: "{}"}})'
169- ' USING INDEX f:func(name)'
170- ' MATCH (f)-[:calls*]->(g) RETURN distinct f, g'
171- .format(program_name, function_calling))
172+ q = (' MATCH (p:program {{name: "{}"}})-[:subject]->(f:func) {}'
173+ ' MATCH (f)-[:calls]->(g:func) RETURN distinct f, g'
174+ .format(program_name, SextantConnection.get_query('f', function_calling)))
175
176 return self._execute_query(program_name, q)
177
178@@ -884,14 +926,10 @@
179 :return: FunctionQueryResult, maximal connected subgraph with leaf function_called
180 """
181
182- if not self.check_function_exists(program_name, function_called):
183- return None
184-
185- q = (' MATCH (p:program {{name: "{}"}})-[:subject]->(g:func {{name: "{}"}})'
186- ' USING INDEX g:func(name)'
187- ' MATCH (f)-[:calls*]->(g) WHERE f.name <> "{}"'
188- ' RETURN distinct f , g')
189- q = q.format(program_name, function_called, program_name)
190+ q = (' MATCH (p:program {{name: "{}"}})-[:subject]->(g:func) {}'
191+ ' MATCH (f)-[:calls]->(g)'
192+ ' RETURN distinct f, g')
193+ q = q.format(program_name, SextantConnection.get_query('g', function_called), program_name)
194
195 return self._execute_query(program_name, q)
196
197@@ -910,22 +948,17 @@
198 if not self.check_program_exists(program_name):
199 return None
200
201- if not self.check_function_exists(program_name, function_called):
202- return None
203-
204- if not self.check_function_exists(program_name, function_calling):
205- return None
206-
207- q = (' MATCH (p:program {{name: "{}"}})-[:subject]->(start:func {{name: "{}"}})'
208- ' USING INDEX start:func(name)'
209- ' MATCH (p)-[:subject]->(end:func {{name: "{}"}})'
210- ' USING INDEX end:func(name)'
211+ start_q = SextantConnection.get_query('start', function_calling)
212+ end_q = SextantConnection.get_query('end', function_called)
213+
214+ q = (' MATCH (p:program {{name: "{}"}})'
215+ ' MATCH (p)-[:subject]->(start:func) {} WITH start, p'
216+ ' MATCH (p)-[:subject]->(end:func) {} WITH start, end'
217 ' MATCH path=(start)-[:calls*]->(end)'
218 ' WITH DISTINCT nodes(path) AS result'
219 ' UNWIND result AS answer'
220 ' RETURN answer')
221- q = q.format(program_name, function_calling, function_called)
222-
223+ q = q.format(program_name, start_q, end_q)
224 return self._execute_query(program_name, q)
225
226 def get_whole_program(self, program_name):
227@@ -942,7 +975,7 @@
228 ' RETURN (f)'.format(program_name))
229 return self._execute_query(program_name, q)
230
231- def get_shortest_path_between_functions(self, program_name, func1, func2):
232+ def get_shortest_path_between_functions(self, program_name, function_calling, function_called):
233 """
234 Execute query to get a single, shortest, path between two functions.
235 :param program_name: string name of the program we wish to search under
236@@ -953,17 +986,16 @@
237 if not self.check_program_exists(program_name):
238 return None
239
240- if not self.check_function_exists(program_name, func1):
241- return None
242-
243- if not self.check_function_exists(program_name, func2):
244- return None
245-
246- q = (' MATCH (p:program {{name: "{}"}})-[:subject]->(f:func {{name: "{}"}})'
247- ' USING INDEX f:func(name)'
248- ' MATCH (p)-[:subject]->(g:func {{name: "{}"}})'
249- ' MATCH path=shortestPath((f)-[:calls*]->(g))'
250- ' UNWIND nodes(path) AS ans'
251- ' RETURN ans'.format(program_name, func1, func2))
252+ start_q = SextantConnection.get_query('start', function_calling)
253+ end_q = SextantConnection.get_query('end', function_called)
254+
255+ q = (' MATCH (p:program {{name: "{}"}})'
256+ ' MATCH (p)-[:subject]->(start:func) {} WITH start, p'
257+ ' MATCH (p)-[:subject]->(end:func) {} WITH start, end'
258+ ' MATCH path=shortestPath((start)-[:calls*]->(end))'
259+ ' UNWIND nodes(path) AS answer'
260+ ' RETURN answer')
261+ q = q.format(program_name, start_q, end_q)
262
263 return self._execute_query(program_name, q)
264+
265
266=== modified file 'src/sextant/objdump_parser.py' (properties changed: +x to -x)
267--- src/sextant/objdump_parser.py 2014-10-23 11:15:48 +0000
268+++ src/sextant/objdump_parser.py 2014-11-18 14:47:39 +0000
269@@ -42,9 +42,12 @@
270 The number of function calls that have been parsed.
271 function_ptr_count:
272 The number of function pointers that have been detected.
273- _known_stubs:
274- A set of the names of functions with type 'stub' that have been
275- parsed - used to avoid registering a stub multiple times.
276+ _known_functions:
277+ A set of the names of functions that have been
278+ parsed - used to avoid registering a function multiple times.
279+ _partial_functions:
280+ A set of functions whose names we have seen but whose source
281+ files we don't yet know.
282
283 """
284 def __init__(self, file_path, file_object=None,
285@@ -102,13 +105,14 @@
286 self.call_count = 0
287 self.function_ptr_count = 0
288
289- # Avoid adding duplicate function stubs (as these are detected from
290- # function calls so may be repeated).
291- self._known_stubs = set()
292+ # Avoid adding duplicate functions.
293+ self._known_functions = set()
294+ # Set of partially-parsed functions.
295+ self._partial_functions = set()
296
297 # By default print information to stdout.
298- def print_func(name, typ):
299- print('func {:25}{}'.format(name, typ))
300+ def print_func(name, typ, source='unknown'):
301+ print('func {:25}{:15}{}'.format(name, typ, source))
302
303 def print_call(caller, callee):
304 print('call {:25}{:25}'.format(caller, callee))
305@@ -116,7 +120,6 @@
306 def print_started(parser):
307 print('parse started: {}[{}]'.format(self.path, ', '.join(self.sections)))
308
309-
310 def print_finished(parser):
311 print('parsed {} functions and {} calls'.format(self.function_count, self.call_count))
312
313@@ -134,12 +137,33 @@
314 self.function_ptr_count += 1
315 return name
316
317- def _add_function_normal(self, name):
318- """
319- Add a function which we have full assembly code for.
320- """
321- self.add_function(name, 'normal')
322- self.function_count += 1
323+ def _add_function(self, name, source=None):
324+ """
325+ Add a partially known or fully known function.
326+ """
327+ if source is None:
328+ # Partial definition - if do not already have a full definition
329+ # for this name then add it to the partials set.
330+ if not name in self._known_functions:
331+ self._partial_functions.add(name)
332+ elif source == 'unknown':
333+ # Manually adding a stub function.
334+ self.add_function(name, 'stub', source)
335+ self.function_count += 1
336+ elif name not in self._known_functions:
337+ # A full definition - either upgrade from partial function
338+ # to known function, or add directly to known functions
339+ # (otherwise we have already seen it)
340+
341+ try:
342+ self._partial_functions.remove(name)
343+ except KeyError:
344+ pass
345+
346+
347+ self._known_functions.add(name)
348+ self.add_function(name, 'normal', source)
349+ self.function_count += 1
350
351 def _add_function_ptr(self, name):
352 """
353@@ -148,15 +172,6 @@
354 self.add_function(name, 'pointer')
355 self.function_count += 1
356
357- def _add_function_stub(self, name):
358- """
359- Add a function stub - we have its name but none of its internals.
360- """
361- if not name in self._known_stubs:
362- self._known_stubs.add(name)
363- self.add_function(name, 'stub')
364- self.function_count += 1
365-
366 def _add_call(self, caller, callee):
367 """
368 Add a function call from caller to callee.
369@@ -171,10 +186,20 @@
370 self.started()
371
372 if self._file is not None:
373- in_section = False # if we are in one of self.sections
374- current_function = None # track the caller for function calls
375+ in_section = False # If we are in one of self.sections.
376+ current_function = None # Track the caller for function calls.
377+ to_add = False
378
379 for line in self._file:
380+ if to_add:
381+ file_line = line.startswith('/')
382+ source = line.split(':')[0] if file_line else None
383+ self._add_function(current_function, source)
384+ to_add = False
385+
386+ if file_line:
387+ continue
388+
389 if line.startswith('Disassembly'):
390 # 'Disassembly of section <name>:\n'
391 section = line.split(' ')[-1].rstrip(':\n')
392@@ -188,13 +213,16 @@
393 # with <function_identifier> of form:
394 # <function_name>[@plt]
395 function_identifier = line.split('<')[-1].split('>')[0]
396+ if function_identifier.startswith('__be_'):
397+ function_identifier = function_identifier.lstrip('__be_')
398
399 if '@' in function_identifier:
400 current_function = function_identifier.split('@')[0]
401- self._add_function_stub(current_function)
402+ self._add_function(current_function)
403 else:
404 current_function = function_identifier
405- self._add_function_normal(current_function)
406+ # Flag function - we look for source on the next line.
407+ to_add = True
408
409 elif 'call ' in line or 'callq ' in line:
410 # WHITESPACE to prevent picking up function names
411@@ -213,9 +241,12 @@
412 # from which we extract name
413 callee_is_ptr = False
414 function_identifier = callee_info.lstrip('<').rstrip('>\n')
415+ if function_identifier.startswith('__be_'):
416+ function_identifier = function_identifier.lstrip('__be_')
417+
418 if '@' in function_identifier:
419 callee = function_identifier.split('@')[0]
420- self._add_function_stub(callee)
421+ self._add_function(callee)
422 else:
423 callee = function_identifier.split('-')[-1].split('+')[0]
424 # Do not add this fn now - it is a normal func
425@@ -231,6 +262,10 @@
426 # Add the call.
427 if not (self.ignore_ptrs and callee_is_ptr):
428 self._add_call(current_function, callee)
429+
430+ for name in self._partial_functions:
431+ self._add_function(name, 'unknown')
432+
433
434 self.finished()
435
436@@ -261,7 +296,7 @@
437 return result
438
439
440-def run_objdump(input_file):
441+def run_objdump(input_file, add_file_paths=False):
442 """
443 Run the objdump command on the file with the given path.
444
445@@ -271,13 +306,24 @@
446 Arguments:
447 input_file:
448 The path of the file to run objdump on.
449+ add_file_paths:
450+ Whether to call with -l option to extract line numbers and source
451+ files from the binary. VERY SLOW on large binaries (~15 hours for ios).
452
453 """
454+ print('input file: {}'.format(input_file))
455 # A single section can be specified for parsing with the -j flag,
456 # but it is not obviously possible to parse multiple sections like this.
457- p = subprocess.Popen(['objdump', '-d', input_file, '--no-show-raw-insn'],
458- stdout=subprocess.PIPE)
459- g = subprocess.Popen(['egrep', 'Disassembly|call(q)? |>:$'], stdin=p.stdout, stdout=subprocess.PIPE)
460+ args = ['objdump', '-d', input_file, '--no-show-raw-insn']
461+ if add_file_paths:
462+ args += ['--line-numbers']
463+
464+ p = subprocess.Popen(args, stdout=subprocess.PIPE)
465+ # Egrep filters out the section headers (Disassembly of section...),
466+ # the call lines (... [l]call[q] ...), the function declarations
467+ # (... <function>:$) and the file paths (^/file_path).
468+ g = subprocess.Popen(['egrep', 'Disassembly|call(q)? |>:$|^/'],
469+ stdin=p.stdout, stdout=subprocess.PIPE)
470 return input_file, g.stdout
471
472
473
474=== modified file 'src/sextant/test_parser.py'
475--- src/sextant/test_parser.py 2014-10-23 11:15:48 +0000
476+++ src/sextant/test_parser.py 2014-11-18 14:47:39 +0000
477@@ -23,7 +23,7 @@
478 calls = defaultdict(list)
479
480 # set the Parser to put output in local dictionaries
481- add_function = lambda n, t: self.add_function(functions, n, t)
482+ add_function = lambda n, t, s='unknown': self.add_function(functions, n, t)
483 add_call = lambda a, b: self.add_call(calls, a, b)
484
485 p = parser.Parser(path, sections=sections, ignore_ptrs=ignore_ptrs,
486
487=== modified file 'src/sextant/test_resources/parser_test'
488Binary files src/sextant/test_resources/parser_test 2014-10-13 14:10:01 +0000 and src/sextant/test_resources/parser_test 2014-11-18 14:47:39 +0000 differ
489=== modified file 'src/sextant/update_db.py'
490--- src/sextant/update_db.py 2014-10-17 14:20:06 +0000
491+++ src/sextant/update_db.py 2014-11-18 14:47:39 +0000
492@@ -20,7 +20,7 @@
493 import logging
494
495 def upload_program(connection, user_name, file_path, program_name=None,
496- not_object_file=False):
497+ not_object_file=False, add_file_paths=False):
498 """
499 Upload a program's functions and call graph to the database.
500
501@@ -38,6 +38,9 @@
502 not_object_file:
503 Flag controlling whether file_path is pointing to a dump file or
504 a binary file.
505+ add_file_paths:
506+ Flag controlling whether to call objdump with the -l option to
507+ extract line numbers and source files. VERY SLOW on large binaries.
508 """
509 if not connection._ssh:
510 raise SSHConnectionError('An SSH connection is required for '
511@@ -59,9 +62,9 @@
512 start = time()
513
514 if not not_object_file:
515- print('Generating dump file...', end='')
516+ print('Generating dump file with{} file paths...'.format(('out', '')[add_file_paths]), end='')
517 sys.stdout.flush()
518- file_path, file_object = run_objdump(file_path)
519+ file_path, file_object = run_objdump(file_path, add_file_paths)
520 print('done.')
521 else:
522 file_object = None
523@@ -82,15 +85,19 @@
524 print('done: {} functions and {} calls.'
525 .format(parser.function_count, parser.call_count))
526
527- parser = Parser(file_path = file_path, file_object = file_object,
528+ parser = Parser(file_path=file_path, file_object = file_object,
529 sections=[],
530- add_function = program.add_function,
531- add_call = program.add_call,
532+ add_function=program.add_function,
533+ add_call=program.add_call,
534 started=lambda parser: start_parser(program),
535 finished=lambda parser: finish_parser(parser, program))
536+
537 parser.parse()
538-
539- program.commit()
540+
541+ if parser.function_count == 0:
542+ print('Nothing to upload. Did you mean to add the --not-object-file flag?')
543+ else:
544+ program.commit()
545
546 end = time()
547 print('Finished in {:.2f}s.'.format(end-start))
548
549=== modified file 'src/sextant/web/server.py'
550--- src/sextant/web/server.py 2014-11-18 14:47:39 +0000
551+++ src/sextant/web/server.py 2014-11-18 14:47:39 +0000
552@@ -13,6 +13,8 @@
553 from twisted.internet.threads import deferToThread
554 from twisted.internet import defer
555
556+from neo4jrestclient.exceptions import TransactionException
557+
558 import logging
559 import os
560 import json
561@@ -24,6 +26,8 @@
562 import tempfile
563 import subprocess
564
565+from datetime import datetime
566+
567 from cgi import escape # deprecated in Python 3 in favour of html.escape, but we're stuck on Python 2
568
569 # global SextantConnection object which deals with the port forwarding
570@@ -174,13 +178,15 @@
571 # if we are okay here we have a valid query with all required arguments
572 if res_code is RESPONSE_CODE_OK:
573 try:
574+ print('running query {}'.format(datetime.now()))
575 program = yield defer_to_thread_with_timeout(render_timeout, fn,
576 name, *req_args)
577- except defer.CancelledError:
578+ print('\tdone {}'.format(datetime.now()))
579+ except Exception as e:
580 # the timeout has fired and cancelled the request
581 res_code = RESPONSE_CODE_BAD_REQUEST
582- res_fmt = "The request timed out after {} seconds."
583- res_msg = res_fmt.format(render_timeout)
584+ res_msg = "{}".format(e)
585+ print('\tfailed {}'.format(datetime.now()))
586
587 if res_code is RESPONSE_CODE_OK:
588 # we have received a response to our request
589@@ -201,10 +207,12 @@
590 suppress_common = suppress_common_arg in ('null', 'true')
591
592 # we have a non-empty return - render it
593+ print('getting plot {}'.format(datetime.now()))
594 res_msg = yield deferToThread(self.get_plot, program,
595 suppress_common,
596 remove_self_calls=False)
597 request.setHeader('content-type', 'image/svg+xml')
598+ print('\tdone {}'.format(datetime.now()))
599
600 request.setResponseCode(res_code)
601 request.write(res_msg)
602@@ -229,6 +237,7 @@
603 max_funcs = AUTOCOMPLETE_NAMES_LIMIT + 1
604 programs = CONNECTION.programs_with_metadata()
605 result = CONNECTION.get_function_names(program_name, search, max_funcs)
606+ print(search, len(result))
607 return result if len(result) < max_funcs else set()
608
609

Subscribers

People subscribed via source and target branches