Coverage for diffoscope/profiling.py: 94%

47 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2024-04-07 13:38 +0000

1# 

2# diffoscope: in-depth comparison of files, archives, and directories 

3# 

4# Copyright © 2016-2017, 2019-2022 Chris Lamb <lamby@debian.org> 

5# 

6# diffoscope is free software: you can redistribute it and/or modify 

7# it under the terms of the GNU General Public License as published by 

8# the Free Software Foundation, either version 3 of the License, or 

9# (at your option) any later version. 

10# 

11# diffoscope is distributed in the hope that it will be useful, 

12# but WITHOUT ANY WARRANTY; without even the implied warranty of 

13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

14# GNU General Public License for more details. 

15# 

16# You should have received a copy of the GNU General Public License 

17# along with diffoscope. If not, see <https://www.gnu.org/licenses/>. 

18 

19import sys 

20import time 

21import contextlib 

22import collections 

23 

24from .utils import format_class 

25from .logging import setup_logging 

26 

27_ENABLED = False 

28 

29 

30@contextlib.contextmanager 

31def profile(namespace, key): 

32 start = time.time() 

33 yield 

34 

35 if _ENABLED: 

36 ProfileManager().increment(start, namespace, key) 

37 

38 

39class ProfileManager: 

40 _singleton = {} 

41 

42 def __init__(self): 

43 self.__dict__ = self._singleton 

44 

45 if not self._singleton: 

46 self.data = collections.defaultdict( 

47 lambda: collections.defaultdict( 

48 lambda: {"time": 0.0, "count": 0} 

49 ) 

50 ) 

51 

52 def setup(self, parsed_args): 

53 global _ENABLED 

54 _ENABLED = parsed_args.profile_output is not None or parsed_args.debug 

55 

56 def increment(self, start, namespace, key): 

57 if not isinstance(key, str): 

58 key = format_class(key.__class__) 

59 

60 self.data[namespace][key]["time"] += time.time() - start 

61 self.data[namespace][key]["count"] += 1 

62 

63 def finish(self, parsed_args=None): 

64 from .presenters.utils import make_printer 

65 

66 # We are being called in the TERM handler so we don't have access to 

67 # parsed_args. Print the profiling output to stderr if we have been 

68 # collecting it. 

69 if parsed_args is None: 

70 if _ENABLED: 

71 self.output(lambda x: print(x, file=sys.stderr)) 

72 return 

73 

74 # Include profiling in --debug output if --profile is not set. 

75 if parsed_args.profile_output is None: 

76 with setup_logging(parsed_args.debug, None) as logger: 

77 self.output(lambda x: logger.debug(x.strip("\n"))) 

78 return 

79 

80 with make_printer(parsed_args.profile_output) as fn: 

81 self.output(fn) 

82 

83 def output(self, print_fn): 

84 title = "# Profiling output for: {}".format(" ".join(sys.argv)) 

85 

86 print_fn(title) 

87 

88 def key(x): 

89 return x[1]["time"] 

90 

91 for namespace, keys in sorted(self.data.items(), key=lambda x: x[0]): 

92 print_fn( 

93 "\n## {} (total time: {:.3f}s)".format( 

94 namespace, sum(x["time"] for x in keys.values()) 

95 ) 

96 ) 

97 

98 for value, totals in sorted(keys.items(), key=key, reverse=True): 

99 print_fn( 

100 " {:10.3f}s {:6d} call{} {}".format( 

101 totals["time"], 

102 totals["count"], 

103 " " if totals["count"] == 1 else "s", 

104 value, 

105 ) 

106 )