Skip to content

Commit ea195b1

Browse files
authored
Merge pull request #6954 from FireLemons/performance_monitoring
reformat stats to conform to discord's message limit
2 parents 2e14061 + a5b59ab commit ea195b1

1 file changed

Lines changed: 200 additions & 16 deletions

File tree

Lines changed: 200 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,217 @@
1+
# frozen_string_literal: true
2+
3+
require "net/http"
14
desc "Post gc stats to discord channel"
25

6+
CODE_BLOCK_DELIMITER_END = "\n...```"
7+
CODE_BLOCK_DELIMITER_START = "```json\n"
8+
DISCORD_MESSAGE_LIMIT = 2000
9+
TOTAL_DELIMITER_LENGTH = CODE_BLOCK_DELIMITER_END.length + CODE_BLOCK_DELIMITER_START.length
10+
311
task post_gc_stat_to_discord: :environment do
4-
require "net/http"
12+
fetch_env_vars => {access_token:, discord_webhook_url:}
13+
14+
stats = fetch_memory_profile(access_token)
15+
message_chunks = chunk_profile_stats(stats)
16+
send_discord_message_chunked(message_chunks)
17+
end
18+
19+
def chunk_long_code_block_message(long_message)
20+
if long_message.length <= DISCORD_MESSAGE_LIMIT
21+
return [long_message]
22+
end
23+
24+
first_chunk, long_message = split_single_chunk(long_message, DISCORD_MESSAGE_LIMIT - CODE_BLOCK_DELIMITER_END.length)
25+
26+
chunks = [first_chunk]
27+
28+
while long_message.length > DISCORD_MESSAGE_LIMIT
29+
puts long_message
30+
chunk, long_message = split_single_chunk(long_message, DISCORD_MESSAGE_LIMIT - TOTAL_DELIMITER_LENGTH)
31+
32+
chunks.push(chunk)
33+
end
34+
35+
chunks.push(long_message)
36+
37+
chunks
38+
end
39+
40+
def chunk_profile_stats(stats_as_json_string)
41+
# parsing the json string consumes memory
42+
old_object_count = get_old_object_count_low_memory_usage(stats_as_json_string)
43+
sample_hour = get_sample_time_low_memory_usage(stats_as_json_string)
44+
45+
largest_old_objects_by_class = get_json_array_value_as_string(stats_as_json_string, "\"largest_old_objects_by_class\": [")
46+
most_common_old_object_classes = get_json_array_value_as_string(stats_as_json_string, "\"most_common_old_object_classes\": [")
47+
most_common_old_strings = get_json_array_value_as_string(stats_as_json_string, "\"most_common_old_strings\": [")
48+
49+
part_1 = <<~MULTILINE
50+
Hour: #{sample_hour}
51+
Old Object Count: #{old_object_count}
52+
53+
Largest Classes in Old Objects
54+
```ruby
55+
#{largest_old_objects_by_class}
56+
```
57+
MULTILINE
558

6-
url = URI("https://casavolunteertracking.org/health/old_objects?token=#{ENV["GC_ACCESS_TOKEN"]}")
59+
part_2 = <<~MULTILINE
60+
Most Common Old Objects
61+
```ruby
62+
#{most_common_old_object_classes}
63+
```
64+
MULTILINE
65+
66+
part_3 = <<~MULTILINE
67+
Most Common Old Strings
68+
```ruby
69+
#{most_common_old_strings}
70+
```
71+
MULTILINE
72+
73+
chunk_long_code_block_message(part_1).concat(chunk_long_code_block_message(part_2)).concat(chunk_long_code_block_message(part_3))
74+
end
75+
76+
def is_valid_discord_message(value)
77+
value.is_a?(String) && value.length <= DISCORD_MESSAGE_LIMIT
78+
end
79+
80+
def enclose_chunked_code_blocks(chunks)
81+
if chunks.length < 2
82+
return chunks
83+
end
84+
85+
chunks[0] += CODE_BLOCK_DELIMITER_END
86+
chunks[-1] = CODE_BLOCK_DELIMITER_START + chunks[-1]
87+
88+
1..chunks.length - 2.each do |i|
89+
chunks[i] = CODE_BLOCK_DELIMITER_START + chunks[i] + CODE_BLOCK_DELIMITER_END
90+
end
91+
end
92+
93+
def fetch_env_vars
94+
access_token = ENV.fetch("GC_ACCESS_TOKEN")
95+
discord_webhook_url = ENV.fetch("DISCORD_WEBHOOK_URL")
96+
97+
{
98+
access_token:,
99+
discord_webhook_url:
100+
}
101+
end
102+
103+
def fetch_memory_profile(access_token)
104+
url = URI("https://casavolunteertracking.org/health/old_objects?token=#{access_token}")
7105
response = Net::HTTP.get_response(url)
8106

9107
unless response.is_a?(Net::HTTPSuccess)
10108
raise "Failed to fetch GC stats. HTTP status code:#{response.code}"
11109
end
12110

13-
stats = response.body
111+
response.body
112+
end
113+
114+
def get_array_closing_bracket_index(opening_bracket_index, stats_as_json_string)
115+
nest_level = 1
116+
117+
search_index = opening_bracket_index + 1
118+
119+
while search_index < stats_as_json_string.length
120+
current_character = stats_as_json_string[search_index]
121+
122+
if current_character == "["
123+
nest_level += 1
124+
elsif current_character == "]"
125+
nest_level -= 1
126+
127+
if nest_level == 0
128+
return search_index
129+
end
130+
end
131+
132+
search_index += 1
133+
end
134+
135+
-1
136+
end
137+
138+
def get_json_array_value_as_string(stats_as_json_string, string_with_key)
139+
start_index = stats_as_json_string.index(string_with_key) + string_with_key.length - 1
140+
stop_index = get_array_closing_bracket_index(start_index, stats_as_json_string)
141+
142+
stats_as_json_string[start_index, stop_index - start_index + 1].gsub("\n ", "\n")
143+
end
144+
145+
def get_old_object_count_low_memory_usage(stats_as_json_string)
146+
old_object_count_marker = '"old_object_count": '
147+
old_object_count_start_index = stats_as_json_string.index(/#{old_object_count_marker}\d+,/) + old_object_count_marker.length
148+
old_object_count_stop_index = old_object_count_start_index
149+
150+
while stats_as_json_string[old_object_count_stop_index] != ","
151+
old_object_count_stop_index += 1
152+
end
153+
154+
stats_as_json_string[old_object_count_start_index, old_object_count_stop_index - old_object_count_start_index]
155+
end
14156

15-
unless ENV["DISCORD_WEBHOOK_URL"].nil?
16-
discord_message = <<~MULTILINE
17-
```json
18-
#{stats}
19-
```
20-
MULTILINE
157+
def get_sample_time_low_memory_usage(stats_as_json_string)
158+
sample_time_marker = 'sample_time": "'
159+
sample_time_start_index = stats_as_json_string.index(/#{sample_time_marker}\d+"/) + sample_time_marker.length
160+
sample_time_count_stop_index = sample_time_start_index
21161

22-
payload = {content: discord_message}.to_json
162+
while stats_as_json_string[sample_time_count_stop_index] != '"'
163+
sample_time_count_stop_index += 1
164+
end
23165

24-
uri = URI.parse(ENV["DISCORD_WEBHOOK_URL"])
25-
http = Net::HTTP.new(uri.host, uri.port)
26-
http.use_ssl = (uri.scheme == "https") # Use SSL for HTTPS
166+
stats_as_json_string[sample_time_start_index, sample_time_count_stop_index - sample_time_start_index]
167+
end
27168

28-
request = Net::HTTP::Post.new(uri.path, {"Content-Type" => "application/json"})
29-
request.body = payload
169+
def send_discord_message(message)
170+
unless is_valid_discord_message(message)
171+
raise ArgumentError, "argument message must be a string with a maximum length of #{DISCORD_MESSAGE_LIMIT} characters"
172+
end
173+
174+
payload = {content: message}.to_json
175+
176+
uri = URI.parse(ENV["DISCORD_WEBHOOK_URL"])
177+
http = Net::HTTP.new(uri.host, uri.port)
178+
http.use_ssl = (uri.scheme == "https")
179+
180+
request = Net::HTTP::Post.new(uri.path, {"Content-Type" => "application/json"})
181+
request.body = payload
182+
183+
http.request(request)
184+
end
185+
186+
def send_discord_message_chunked(message_chunks)
187+
message_chunks.each do |chunk|
188+
request_result = send_discord_message(chunk)
189+
verify_discord_message_posted(request_result)
190+
sleep 0.5
191+
end
192+
end
193+
194+
def split_single_chunk(message, max_chunk_size)
195+
if message.length <= max_chunk_size
196+
return ["", message]
197+
end
198+
199+
last_newline_index = max_chunk_size - 1
200+
201+
while last_newline_index > 0 && message[last_newline_index] != "\n"
202+
last_newline_index -= 1
203+
end
204+
205+
if last_newline_index == 0
206+
[message[0, max_chunk_size], message[max_chunk_size, message.length]]
207+
else
208+
[message[0, last_newline_index], message[last_newline_index + 1, message.length]]
209+
end
210+
end
30211

31-
http.request(request)
212+
def verify_discord_message_posted(request_result)
213+
status_code_as_number = Integer(request_result.code)
214+
unless status_code_as_number >= 200 && status_code_as_number < 300
215+
raise "Failed to send discord message. HTTP status code:#{request_result.code}"
32216
end
33217
end

0 commit comments

Comments
 (0)