Data pre-processing and Exploratory Data Analysis
Tiền xử lí số liệu (Data pre-processing) và phân tích khám phá dữ liệu EDA (Exploratory Data Analysis) là bước luôn phải được thực hiện đầu tiên của một data project. Trước hết đọc hai files dữ liệu movies.csv và ratings.csv:
# Clear workspace:
rm(list = ls())
# Load data:
library(tidyverse)
movies <- read_csv("C:/Users/Admin/Documents/ml-latest-small/movies.csv")
ratings <- read_csv("C:/Users/Admin/Documents/ml-latest-small/ratings.csv")
Bộ dữ liệu movies miêu tả về phim (tên phim + thể loại) và mã movieId tương ứng của phim. Còn bộ dữ liệu ratings là xếp hạng của người dùng đối với một bộ phim mà họ đã xem. Chúng ta có thể xem qua những dữ liệu này:
# Show some observations:
library(knitr)
kable(movies %>% head(), caption = "Table 1: Movie data")
Table 1: Movie data
1 |
Toy Story (1995) |
Adventure|Animation|Children|Comedy|Fantasy |
2 |
Jumanji (1995) |
Adventure|Children|Fantasy |
3 |
Grumpier Old Men (1995) |
Comedy|Romance |
4 |
Waiting to Exhale (1995) |
Comedy|Drama|Romance |
5 |
Father of the Bride Part II (1995) |
Comedy |
6 |
Heat (1995) |
Action|Crime|Thriller |
kable(ratings %>% head(), caption = "Table 2: Rating data")
Table 2: Rating data
1 |
1 |
4 |
964982703 |
1 |
3 |
4 |
964981247 |
1 |
6 |
4 |
964982224 |
1 |
47 |
5 |
964983815 |
1 |
50 |
5 |
964982931 |
1 |
70 |
3 |
964982400 |
Riêng cột biến timestamp là ở dạng Epoch time và chúng ta có thể chuyển về date time để thực hiện những phân tích xa hơn nếu cần:
# Convert to real date time:
library(lubridate)
ratings %>%
mutate(real_time = as_datetime(timestamp), date_ymd = date(real_time)) -> ratings
Cột biến genres mô tả thể loại của phim. Để thuận lợi cho các phân tích sau này thì no genres listed nên thay bằng Other và dấu - được thay bằng _ như sau:
# Relabel for some genres:
movies %>%
mutate(genres = genres %>%
str_replace_all("\\(no genres listed\\)", "Other") %>%
str_replace_all("-", "_")) -> movies
Chúng ta kì vọng rằng số lượng các movieId ở hai bộ số liệu là trùng nhau nhưng thực tế không phải vậy:
ratings$movieId %>% n_distinct()
## [1] 9724
movies$movieId %>% n_distinct()
## [1] 9742
Do vậy chúng ta sẽ chỉ lấy ra những bộ phim mà thuộc về cả hai bộ dữ liệu ratings và movies:
# Common movieId:
base::intersect(ratings$movieId %>% unique(), movies$movieId %>% unique()) -> common_movies
# Filter data by common_movies:
movies %>% filter(movieId %in% common_movies) -> movies_common
ratings %>% filter(movieId %in% common_movies) -> ratings_common
# Combine data:
full_join(movies_common, ratings_common, by = "movieId") -> movie_data
# Some observations:
movie_data %>% head()
## # A tibble: 6 x 8
## movieId title genres userId rating timestamp real_time date_ymd
## <dbl> <chr> <chr> <dbl> <dbl> <dbl> <dttm> <date>
## 1 1 Toy S~ Adventu~ 1 4 9.65e8 2000-07-30 18:45:03 2000-07-30
## 2 1 Toy S~ Adventu~ 5 4 8.47e8 1996-11-08 06:36:02 1996-11-08
## 3 1 Toy S~ Adventu~ 7 4.5 1.11e9 2005-01-25 06:52:26 2005-01-25
## 4 1 Toy S~ Adventu~ 15 2.5 1.51e9 2017-11-13 12:59:30 2017-11-13
## 5 1 Toy S~ Adventu~ 17 4.5 1.31e9 2011-05-18 05:28:03 2011-05-18
## 6 1 Toy S~ Adventu~ 18 3.5 1.46e9 2016-02-11 16:56:56 2016-02-11
Như đã đề cập CFR là cách tiếp cận mà chỉ căn cứ hành vi của user chứ không sử dụng thông tin về content của phim (hay sách). Điều này có nghĩa là cột biến genres về thể loại phim (có thể xem như đây là một dạng content đơn giản) sẽ không sử dụng khi xây dựng CFR. Tuy nhiên chúng ta có thể sử dụng genres sau này. Vậy nên tiện thể chúng ta viết một hàm có tên convert_to_01_content để covert về ma trận nhị phân (binary matrix)
convert_to_01_content <- function(genres) {
genres %>%
str_split(pattern = "\\|", simplify = TRUE) %>%
data.frame() -> df_ith
names(df_ith) <- df_ith %>% slice(1)
return(df_ith %>% mutate(genres = genres))
}
Sử dụng hàm này cho cột biến genres đồng thời tạo binary matrix/data frame:
lapply(movie_data$genres, convert_to_01_content) -> df_genres
do.call("bind_rows", df_genres) -> df_genres
# Convert to binary matrix/data frame:
df_genres %>%
select(-genres) %>%
mutate_all(function(x) {case_when(is.na(x) ~ 0, TRUE ~ 1)}) -> df_genres01
# Combine data sets:
movie_data %>% bind_cols(df_genres01) -> movie_data_01
# Show some observations:
movie_data_01 %>% head()
## # A tibble: 6 x 28
## movieId title genres userId rating timestamp real_time date_ymd
## <dbl> <chr> <chr> <dbl> <dbl> <dbl> <dttm> <date>
## 1 1 Toy ~ Adven~ 1 4 9.65e8 2000-07-30 18:45:03 2000-07-30
## 2 1 Toy ~ Adven~ 5 4 8.47e8 1996-11-08 06:36:02 1996-11-08
## 3 1 Toy ~ Adven~ 7 4.5 1.11e9 2005-01-25 06:52:26 2005-01-25
## 4 1 Toy ~ Adven~ 15 2.5 1.51e9 2017-11-13 12:59:30 2017-11-13
## 5 1 Toy ~ Adven~ 17 4.5 1.31e9 2011-05-18 05:28:03 2011-05-18
## 6 1 Toy ~ Adven~ 18 3.5 1.46e9 2016-02-11 16:56:56 2016-02-11
## # ... with 20 more variables: Adventure <dbl>, Animation <dbl>, Children <dbl>,
## # Comedy <dbl>, Fantasy <dbl>, Romance <dbl>, Drama <dbl>, Action <dbl>,
## # Crime <dbl>, Thriller <dbl>, Horror <dbl>, Mystery <dbl>, Sci_Fi <dbl>,
## # War <dbl>, Musical <dbl>, Documentary <dbl>, IMAX <dbl>, Western <dbl>,
## # Film_Noir <dbl>, Other <dbl>
Chúng ta có thể rút ra, ví dụ, một số insights về hành vi tiêu dùng/xem phim của các users:
# Most popular genres:
df_genres01 %>%
gather(Genre, n) %>%
group_by(Genre) %>%
summarise(Freq = sum(n)) %>%
arrange(Freq) %>%
mutate(Genre = factor(Genre, levels = Genre)) %>%
ggplot(aes(Genre, Freq)) +
geom_col() +
coord_flip() +
labs(x = NULL, y = NULL, title = "Figure 1: Most Viewed Movies")

# Genre trend:
movie_data_01 %>%
select(-c(1:7)) %>%
gather(genres, count, -date_ymd) %>%
group_by(date_ymd, genres) %>%
summarise(total = sum(count)) %>%
ggplot(aes(date_ymd, total, color = genres)) +
geom_line(show.legend = FALSE) +
facet_wrap(~ genres, scales = "free_x") +
labs(x = NULL, y = NULL, title = "Figure 2: Trend of Viewed Movies")

# Most watched movies (top 10):
movie_data_01 %>%
group_by(title) %>%
count() %>%
arrange(-n) %>%
head(10) %>%
kable(caption = "Table 3: Top-10 Movies")
Table 3: Top-10 Movies
Forrest Gump (1994) |
329 |
Shawshank Redemption, The (1994) |
317 |
Pulp Fiction (1994) |
307 |
Silence of the Lambs, The (1991) |
279 |
Matrix, The (1999) |
278 |
Star Wars: Episode IV - A New Hope (1977) |
251 |
Jurassic Park (1993) |
238 |
Braveheart (1995) |
237 |
Terminator 2: Judgment Day (1991) |
224 |
Schindler’s List (1993) |
220 |
Không nằm ngoài dự đoán, Shawshank Redemption (bộ phim ưa thích của người viết bài này) có số lượt xem xếp thứ hai chỉ sau phim dành giải Oscar năm 1994 Forrest Gump. Chúng ta tìm hiểu thêm về một số thông tin có thể là hữu ích thu được từ bộ dữ liệu. Ví dụ: tổng số lượt người xem phim là 100836 cho tất cả 9724 bộ phim, như vậy bình quân một bộ phim được xem chừng 10.37 lần nhưng phân bố rất lệch. Thực vậy, chỉ có 450 bộ phim có số lượt xem nhiều hơn 50:
movie_data_01 %>%
group_by(title) %>%
count() %>%
filter(n >= 50) %>%
arrange(-n) -> movies_over50_viewed
kable(movies_over50_viewed %>% head(), caption = "Table 4: Top 450 Movies")
Table 4: Top 450 Movies
Forrest Gump (1994) |
329 |
Shawshank Redemption, The (1994) |
317 |
Pulp Fiction (1994) |
307 |
Silence of the Lambs, The (1991) |
279 |
Matrix, The (1999) |
278 |
Star Wars: Episode IV - A New Hope (1977) |
251 |
Figure 3 cho thấy rating = 4 là xếp hạng phổ biến nhất của người dùng dành cho các bộ phim:
movie_data_01 %>%
group_by(rating) %>%
count() %>%
ungroup() %>%
mutate(rating = as.factor(rating)) %>%
ggplot(aes(rating, n)) +
geom_col() +
labs(x = NULL, y = NULL, title = "Figure 3: Distribution of Rating")

Recommender based on Ratings: Item-based CFR
CFR có thể được xây dựng dựa vào xếp hạng (ratings) mà user bình bầu cho bộ phim họ đã xem. Nhưng trước hết chúng ta cần convert xếp hạng của những bộ phim bởi user (người xem) về ma trận thưa (sparse matrix):
# Convert to sparse matrix for ratings:
ratings %>%
select(movieId, userId, rating) %>%
spread(value = rating, key = movieId) -> sparse_df
Vì có 610 users và 9724 bộ phim nên sparse matrix sẽ nên là một ma trận có 610 dòng và 9724 cột. Kết quả sau đây cho thấy sparse data frame dư ra một cột là userId (các cột biến khác chính là movieId - hay là các items):
head(sparse_df[1:6, 1:6])
## # A tibble: 6 x 6
## userId `1` `2` `3` `4` `5`
## <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 1 4 NA 4 NA NA
## 2 2 NA NA NA NA NA
## 3 3 NA NA NA NA NA
## 4 4 NA NA NA NA NA
## 5 5 4 NA NA NA NA
## 6 6 NA 4 5 3 5
Do vậy chúng ta cần loại bỏ cột biến userId này trước khi convert về sparse matrix bằng hàm as()
như sau:
# Remove userId and convert to matrix:
sparse_df %>%
select(-userId) %>%
as.matrix() -> ratingmat
# Convert to realRatingMatrix class for modeling Recommendation System:
library(recommenderlab)
as(ratingmat, "realRatingMatrix") -> ratingmat
CFR vận hành dựa trên thước đo gọi là sự tương tự (similarity) hay “khoảng cách” giữa các: (1) users, (2) hoặc items và được tính toán theo ba cách khác nhau: (1) cosine similarity, (2) Pearson similarity, và (3) Jaccard similarity. Thước đo này được tính toán bằng cách sử dụng hàm similarity()
của thư viện recommenderlab như sau:
# Calculate cosine similarity for 10 observation by user:
similarity_users <- similarity(ratingmat[1:10, ], method = "cosine", which = "users")
# Plot cosine similarity heatmap:
image(as.matrix(similarity_users), main = "Figure 4: Similarity by User")

Hoặc them items:
# Calculate cosine similarity for 10 observation by item:
similarity_items <- similarity(ratingmat[, 1:10], method = "cosine", which = "items")
image(as.matrix(similarity_items), main = "Figure 5: Similarity by Item")

Tương tự như vậy là heatmap cho rating mà ở đó mỗi một dòng là một user còn mỗi một cột là movie. Ví dụ, 20 users + 20 movies đầu tiên:
image(ratingmat[1:20, 1:20], main = "Figure 6: Heatmap of the first 20 rows and 20 columns")

Sử dụng 80% data để train CFR và phần dữ liệu còn lại để test CFR:
set.seed(1)
id <- sample(x = 1:nrow(ratingmat), size = 0.8*nrow(ratingmat), replace = FALSE)
data_train <- ratingmat[id, ]
data_test <- ratingmat[-id, ]
Huấn luyện CFR Engine:
# Define some parameters for CFR Engine:
my_params <- list(k = 30, method = "Cosine")
# Train CFR recommender:
cfr_recommender <- Recommender(data = data_train,
method = "IBCF",
parameter = my_params)
Sử dụng CFR Engine đã có để đưa ra recommendations cho các users:
# Use CFR recommender for test data.
n_recommended <- 3
items_predicted <- predict(object = cfr_recommender,
newdata = data_test,
n = n_recommended)
Chúng ta cũng có thể list danh sách 20 bộ phim được đề xuất nhiều nhất bởi CFR Engine (Nixon (1995) là bộ phim được khuyến nghị nhiều lần nhất bởi CFR Engine với 10 lần):
data.frame(movieId = unlist(items_predicted@items)) %>%
group_by(movieId) %>%
count() %>%
ungroup() %>%
top_n(n = 20, wt = n) %>%
arrange(n) %>%
inner_join(movies_common %>% select(1:2)) %>%
mutate(title = factor(title, levels = title)) -> top_20_recommended
top_20_recommended %>%
ggplot(aes(title, n)) +
geom_col() +
geom_text(aes(label = n), hjust = 1.3, color = "white") +
coord_flip() +
labs(x = NULL, y = NULL, title = "Figure 7: Top-recommended Movies by CFR")

Xem 3 bộ phim mà Engine khuyến nghị cho, ví dụ, user thứ nhất (trong test data):
movies_recommended_user1 <- items_predicted@items[[1]]
movies_common %>%
filter(movieId %in% movies_recommended_user1) %>%
select(movieId, title)
## # A tibble: 3 x 2
## movieId title
## <dbl> <chr>
## 1 9 Sudden Death (1995)
## 2 71 Fair Game (1995)
## 3 105 Bridges of Madison County, The (1995)
LS0tDQp0aXRsZTogJ1JlY29tbWVuZGF0aW9uIFN5c3RlbSAoUGFydCAxKScNCmF1dGhvcjogJ05ndXllbiBDaGkgRHVuZycNCnN1YnRpdGxlOiAiUiBNYWNoaW5lIExlYXJuaW5nIFNlcmllcyINCm91dHB1dDoNCiAgaHRtbF9kb2N1bWVudDogDQogICAgY29kZV9kb3dubG9hZDogdHJ1ZQ0KICAgICMgY29kZV9mb2xkaW5nOiBoaWRlDQogICAgaGlnaGxpZ2h0OiB6ZW5idXJuDQogICAgIyBudW1iZXJfc2VjdGlvbnM6IHllcw0KICAgIHRoZW1lOiAiZmxhdGx5Ig0KICAgIHRvYzogVFJVRQ0KICAgIHRvY19mbG9hdDogVFJVRQ0KLS0tDQoNCmBgYHtyIHNldHVwLGluY2x1ZGU9RkFMU0V9DQprbml0cjo6b3B0c19jaHVuayRzZXQoZWNobyA9IFRSVUUsIHdhcm5pbmcgPSBGQUxTRSwgbWVzc2FnZSA9IEZBTFNFLCBmaWcud2lkdGggPSAxMCwgZmlnLmhlaWdodCA9IDYpDQpgYGANCg0KDQojIEludHJvZHVjdGlvbiB0byBDb2xsYWJvcmF0aXZlIEZpbHRlcmluZyBSZWNvbW1lbmRlcg0KDQpI4buHIHRo4buRbmcga2h1eeG6v24gbmdo4buLIChSZWNvbW1lbmRhdGlvbiBTeXN0ZW0pIGzDoCBt4buZdCBFbmdpbmUgxJHGsOG7o2Mgc+G7rSBk4bulbmcgcGjhu5UgYmnhur9uIOG7nyBuaGnhu4F1IGPDtG5nIHRpIHRoxrDGoW5nIG3huqFpIMSRaeG7h24gdOG7rS4gQ29sbGFib3JhdGl2ZSBGaWx0ZXJpbmcgUmVjb21tZW5kZXIgKENGUikgbMOgIG3hu5l0IGPDoWNoIHRp4bq/cCBj4bqtbiAobcO0IGjDrG5oKSBk4buxYSB0csOqbiBk4buvIGxp4buHdSBs4buLY2ggc+G7rSB24buBIGjDoG5oIHZpIG11YSBz4bqvbSBj4bunYSBraMOhY2ggaMOgbmcgduG7m2kgw70gdMaw4bufbmcgcuG6sW5nIG7hur91LCB2w60gZOG7pSwgaGFpIGtow6FjaCBow6BuZyBjw7luZyBtdWEgbeG7mXQgY3Xhu5FuIHPDoWNoIChob+G6t2MgeGVtIGPDuW5nIG3hu5l0IGLhu5kgcGhpbSkgdGjDrCBy4bqldCBjw7MgdGjhu4MgaOG7jSBz4bq9IHTDoWkgZGnhu4VuIHBhdHRlcm4gxJHDsyB0cm9uZyB0xrDGoW5nIGxhaS4gSG/hurdjIGhhaSBraMOhY2ggaMOgbmcgQSB2w6AgQiDEkcaw4bujYyBjaG8gbMOgIGPDsyBow6BuaCB2aSBtdWEgc+G6r20gdMawxqFuZyB04buxIG5oYXUgdGjDrCBu4bq/dSBraMOhY2ggaMOgbmcgQSDEkcOjIHhlbSBt4buZdCBi4buZIHBoaW0gWCBuw6BvIMSRw7MgbmjGsG5nIEIgdGjDrCBs4bqhaSBjaMawYSB4ZW0gYuG7mSBwaGltIG7DoHkgdGjDrCBu4bq/dSBo4buHIHRo4buRbmcgZ+G7o2kgw70gcuG6sW5nIG7Dqm4gZ2nhu5tpIHRoaeG7h3UgcGhpbSBYIG7DoHkgY2hvIGtow6FjaCBow6BuZyBCLiBDw6FjaCB0aeG6v3AgY+G6rW4gbsOgeSByw7UgcsOgbmcgY2jhu4kgZOG7sWEgdHLDqm4gc+G7nyB0aMOtY2ggY+G7p2Ega2jDoWNoIGjDoG5nICh1c2VyIHByZWZlcmVuY2VzKSBjaOG7qSBraMO0bmcgZOG7sWEgdHLDqm4gbmjhu69uZyDEkeG6t2MgxJFp4buDbSBoYXkgbuG7mWkgZHVuZyAoY29udGVudHMpIGPhu6dhIHPDoWNoL3BoaW0gxJHhu4MgxJHGsGEgcmEga2h1eeG6v24gbmdo4buLIGNobyBuZ8aw4budaSBkw7luZy9raMOhY2ggaMOgbmcgdGnhu4FtIG7Eg25nLiANCg0KDQpCw6BpIHZp4bq/dCBuw6B5IG1pbmggaOG7jWEgdmnhu4djIHjDonkgZOG7sW5nIENGUiBi4bqxbmcgbmfDtG4gbmfhu68gUiB24bubaSBi4buZIGThu68gbGnhu4d1IHThu6sgTW92aWVMZW5zIChwaGnDqm4gYuG6o24gc21hbGwpIGJhbyBn4buTbSAxMDUzMzkgcmF0aW5ncyBj4bunYSAxMDMyOSBi4buZIHBoaW0gdOG7qyA2NjggdXNlciB0cm9uZyBraG/huqNuZyB0aOG7nWkgZ2lhbiB04burIEFwcmlsIDAzLCAxOTk2IMSR4bq/biBKYW51YXJ5IDA5LCAyMDE2LiBC4buZIGThu68gbGnhu4d1IG7DoHkgY8OzIHRo4buDIFtkb3dubG9hZCDhu58gxJHDonldKGh0dHA6Ly9ncm91cGxlbnMub3JnL2RhdGFzZXRzL21vdmllbGVucy9sYXRlc3QpLiDEkOG7i25oIGjGsOG7m25nIHRo4buxYyBow6BuaCBuw6puIGPDoWMgbMOtIHRodXnhur90IHbDoCBjxqEgc+G7nyB0b8OhbiBo4buNYyB24buBIFJlY29tbWVuZGF0aW9uIFN5c3RlbSBz4bq9IGtow7RuZyDEkcaw4bujYyB0csOsbmggYsOgeSDhu58gxJHDonkuIELhuqFuIMSR4buNYyDEkcaw4bujYyBnaeG6oyDEkeG7i25oIGzDoCDEkcOjIGPDsyBuaOG7r25nIGhp4buDdSBiaeG6v3QgbmjhuqV0IMSR4buLbmggduG7gSBSZWNvbW1lbmRhdGlvbiBTeXN0ZW0uIA0KDQojIERhdGEgcHJlLXByb2Nlc3NpbmcgYW5kIEV4cGxvcmF0b3J5IERhdGEgQW5hbHlzaXMgDQoNClRp4buBbiB44butIGzDrSBz4buRIGxp4buHdSAoRGF0YSBwcmUtcHJvY2Vzc2luZykgdsOgIHBow6JuIHTDrWNoIGtow6FtIHBow6EgZOG7ryBsaeG7h3UgRURBIChFeHBsb3JhdG9yeSBEYXRhIEFuYWx5c2lzKSBsw6AgYsaw4bubYyBsdcO0biBwaOG6o2kgxJHGsOG7o2MgdGjhu7FjIGhp4buHbiDEkeG6p3UgdGnDqm4gY+G7p2EgbeG7mXQgZGF0YSBwcm9qZWN0LiBUcsaw4bubYyBo4bq/dCDEkeG7jWMgaGFpIGZpbGVzIGThu68gbGnhu4d1ICoqbW92aWVzLmNzdioqIHbDoCAqKnJhdGluZ3MuY3N2Kio6IA0KDQpgYGB7cn0NCg0KIyBDbGVhciB3b3Jrc3BhY2U6IA0KDQpybShsaXN0ID0gbHMoKSkNCg0KIyBMb2FkIGRhdGE6IA0KDQpsaWJyYXJ5KHRpZHl2ZXJzZSkNCg0KbW92aWVzIDwtIHJlYWRfY3N2KCJDOi9Vc2Vycy9BZG1pbi9Eb2N1bWVudHMvbWwtbGF0ZXN0LXNtYWxsL21vdmllcy5jc3YiKQ0KDQpyYXRpbmdzIDwtIHJlYWRfY3N2KCJDOi9Vc2Vycy9BZG1pbi9Eb2N1bWVudHMvbWwtbGF0ZXN0LXNtYWxsL3JhdGluZ3MuY3N2IikNCg0KYGBgDQoNCkLhu5kgZOG7ryBsaeG7h3UgbW92aWVzIG1pw6p1IHThuqMgduG7gSBwaGltICh0w6puIHBoaW0gKyB0aOG7gyBsb+G6oWkpIHbDoCBtw6MgbW92aWVJZCB0xrDGoW5nIOG7qW5nIGPhu6dhIHBoaW0uIEPDsm4gYuG7mSBk4buvIGxp4buHdSByYXRpbmdzIGzDoCB44bq/cCBo4bqhbmcgY+G7p2EgbmfGsOG7nWkgZMO5bmcgxJHhu5FpIHbhu5tpIG3hu5l0IGLhu5kgcGhpbSBtw6AgaOG7jSDEkcOjIHhlbS4gQ2jDum5nIHRhIGPDsyB0aOG7gyB4ZW0gcXVhIG5o4buvbmcgZOG7ryBsaeG7h3UgbsOgeTogDQoNCmBgYHtyfQ0KDQojIFNob3cgc29tZSBvYnNlcnZhdGlvbnM6IA0KDQpsaWJyYXJ5KGtuaXRyKQ0KDQprYWJsZShtb3ZpZXMgJT4lIGhlYWQoKSwgY2FwdGlvbiA9ICJUYWJsZSAxOiBNb3ZpZSBkYXRhIikNCg0Ka2FibGUocmF0aW5ncyAlPiUgaGVhZCgpLCBjYXB0aW9uID0gIlRhYmxlIDI6IFJhdGluZyBkYXRhIikNCmBgYA0KDQpSacOqbmcgY+G7mXQgYmnhur9uIHRpbWVzdGFtcCBsw6Ag4bufIGThuqFuZyBFcG9jaCB0aW1lIHbDoCBjaMO6bmcgdGEgY8OzIHRo4buDIGNodXnhu4NuIHbhu4EgZGF0ZSB0aW1lIMSR4buDIHRo4buxYyBoaeG7h24gbmjhu69uZyBwaMOibiB0w61jaCB4YSBoxqFuIG7hur91IGPhuqduOiANCg0KDQpgYGB7cn0NCiMgQ29udmVydCB0byByZWFsIGRhdGUgdGltZToNCg0KbGlicmFyeShsdWJyaWRhdGUpDQoNCnJhdGluZ3MgJT4lIA0KICBtdXRhdGUocmVhbF90aW1lID0gYXNfZGF0ZXRpbWUodGltZXN0YW1wKSwgZGF0ZV95bWQgPSBkYXRlKHJlYWxfdGltZSkpIC0+IHJhdGluZ3MNCmBgYA0KDQpD4buZdCBiaeG6v24gZ2VucmVzIG3DtCB04bqjIHRo4buDIGxv4bqhaSBj4bunYSBwaGltLiDEkOG7gyB0aHXhuq1uIGzhu6NpIGNobyBjw6FjIHBow6JuIHTDrWNoIHNhdSBuw6B5IHRow6wgKm5vIGdlbnJlcyBsaXN0ZWQqIG7Dqm4gdGhheSBi4bqxbmcgT3RoZXIgdsOgIGThuqV1IC0gxJHGsOG7o2MgdGhheSBi4bqxbmcgXyBuaMawIHNhdTogDQoNCmBgYHtyfQ0KIyBSZWxhYmVsIGZvciBzb21lIGdlbnJlczogDQoNCm1vdmllcyAlPiUgDQogIG11dGF0ZShnZW5yZXMgPSBnZW5yZXMgJT4lIA0KICAgICAgICAgICBzdHJfcmVwbGFjZV9hbGwoIlxcKG5vIGdlbnJlcyBsaXN0ZWRcXCkiLCAiT3RoZXIiKSAlPiUgDQogICAgICAgICAgIHN0cl9yZXBsYWNlX2FsbCgiLSIsICJfIikpIC0+IG1vdmllcw0KYGBgDQoNCkNow7puZyB0YSBrw6wgduG7jW5nIHLhurFuZyBz4buRIGzGsOG7o25nIGPDoWMgbW92aWVJZCDhu58gaGFpIGLhu5kgc+G7kSBsaeG7h3UgbMOgIHRyw7luZyBuaGF1IG5oxrBuZyB0aOG7sWMgdOG6vyBraMO0bmcgcGjhuqNpIHbhuq15OiANCg0KYGBge3J9DQoNCnJhdGluZ3MkbW92aWVJZCAlPiUgbl9kaXN0aW5jdCgpDQoNCm1vdmllcyRtb3ZpZUlkICU+JSBuX2Rpc3RpbmN0KCkNCmBgYA0KDQpEbyB24bqteSBjaMO6bmcgdGEgc+G6vSBjaOG7iSBs4bqleSByYSBuaOG7r25nIGLhu5kgcGhpbSBtw6AgdGh14buZYyB24buBIGPhuqMgaGFpIGLhu5kgZOG7ryBsaeG7h3UgcmF0aW5ncyB2w6AgbW92aWVzOiANCg0KYGBge3J9DQoNCiMgQ29tbW9uIG1vdmllSWQ6IA0KDQpiYXNlOjppbnRlcnNlY3QocmF0aW5ncyRtb3ZpZUlkICU+JSB1bmlxdWUoKSwgbW92aWVzJG1vdmllSWQgJT4lIHVuaXF1ZSgpKSAtPiBjb21tb25fbW92aWVzDQoNCiMgRmlsdGVyIGRhdGEgYnkgY29tbW9uX21vdmllczogDQoNCm1vdmllcyAlPiUgZmlsdGVyKG1vdmllSWQgJWluJSBjb21tb25fbW92aWVzKSAtPiBtb3ZpZXNfY29tbW9uDQoNCnJhdGluZ3MgJT4lIGZpbHRlcihtb3ZpZUlkICVpbiUgY29tbW9uX21vdmllcykgLT4gcmF0aW5nc19jb21tb24NCg0KIyBDb21iaW5lIGRhdGE6IA0KDQpmdWxsX2pvaW4obW92aWVzX2NvbW1vbiwgcmF0aW5nc19jb21tb24sIGJ5ID0gIm1vdmllSWQiKSAtPiBtb3ZpZV9kYXRhDQoNCiMgU29tZSBvYnNlcnZhdGlvbnM6IA0KDQptb3ZpZV9kYXRhICU+JSBoZWFkKCkNCg0KYGBgDQoNCk5oxrAgxJHDoyDEkeG7gSBj4bqtcCBDRlIgbMOgIGPDoWNoIHRp4bq/cCBj4bqtbiBtw6AgY2jhu4kgY8SDbiBj4bupIGjDoG5oIHZpIGPhu6dhIHVzZXIgY2jhu6kga2jDtG5nIHPhu60gZOG7pW5nIHRow7RuZyB0aW4gduG7gSBjb250ZW50IGPhu6dhIHBoaW0gKGhheSBzw6FjaCkuIMSQaeG7gXUgbsOgeSBjw7MgbmdoxKlhIGzDoCBj4buZdCBiaeG6v24gZ2VucmVzIHbhu4EgdGjhu4MgbG/huqFpIHBoaW0gKGPDsyB0aOG7gyB4ZW0gbmjGsCDEkcOieSBsw6AgbeG7mXQgZOG6oW5nIGNvbnRlbnQgxJHGoW4gZ2nhuqNuKSBz4bq9IGtow7RuZyBz4butIGThu6VuZyBraGkgeMOieSBk4buxbmcgQ0ZSLiBUdXkgbmhpw6puIGNow7puZyB0YSBjw7MgdGjhu4Mgc+G7rSBk4bulbmcgZ2VucmVzIHNhdSBuw6B5LiBW4bqteSBuw6puIHRp4buHbiB0aOG7gyBjaMO6bmcgdGEgdmnhur90IG3hu5l0IGjDoG0gY8OzIHTDqm4gKipjb252ZXJ0X3RvXzAxX2NvbnRlbnQqKiDEkeG7gyBjb3ZlcnQgduG7gSBtYSB0cuG6rW4gbmjhu4sgcGjDom4gKGJpbmFyeSBtYXRyaXgpDQoNCmBgYHtyfQ0KY29udmVydF90b18wMV9jb250ZW50IDwtIGZ1bmN0aW9uKGdlbnJlcykgew0KICANCiAgZ2VucmVzICU+JSANCiAgICBzdHJfc3BsaXQocGF0dGVybiA9ICJcXHwiLCBzaW1wbGlmeSA9IFRSVUUpICU+JSANCiAgICBkYXRhLmZyYW1lKCkgLT4gZGZfaXRoDQogIA0KICBuYW1lcyhkZl9pdGgpIDwtIGRmX2l0aCAlPiUgc2xpY2UoMSkgDQogIA0KICByZXR1cm4oZGZfaXRoICU+JSBtdXRhdGUoZ2VucmVzID0gZ2VucmVzKSkNCn0NCg0KYGBgDQoNClPhu60gZOG7pW5nIGjDoG0gbsOgeSBjaG8gY+G7mXQgYmnhur9uIGdlbnJlcyDEkeG7k25nIHRo4budaSB04bqhbyBiaW5hcnkgbWF0cml4L2RhdGEgZnJhbWU6IA0KDQpgYGB7cn0NCg0KbGFwcGx5KG1vdmllX2RhdGEkZ2VucmVzLCBjb252ZXJ0X3RvXzAxX2NvbnRlbnQpIC0+IGRmX2dlbnJlcw0KDQpkby5jYWxsKCJiaW5kX3Jvd3MiLCBkZl9nZW5yZXMpIC0+IGRmX2dlbnJlcw0KDQojIENvbnZlcnQgdG8gYmluYXJ5IG1hdHJpeC9kYXRhIGZyYW1lOiANCg0KZGZfZ2VucmVzICU+JSANCiAgc2VsZWN0KC1nZW5yZXMpICU+JSANCiAgbXV0YXRlX2FsbChmdW5jdGlvbih4KSB7Y2FzZV93aGVuKGlzLm5hKHgpIH4gMCwgVFJVRSB+IDEpfSkgLT4gZGZfZ2VucmVzMDENCg0KIyBDb21iaW5lIGRhdGEgc2V0czogDQoNCm1vdmllX2RhdGEgJT4lIGJpbmRfY29scyhkZl9nZW5yZXMwMSkgLT4gbW92aWVfZGF0YV8wMSANCg0KIyBTaG93IHNvbWUgb2JzZXJ2YXRpb25zOiANCg0KbW92aWVfZGF0YV8wMSAlPiUgaGVhZCgpDQoNCmBgYA0KDQpDaMO6bmcgdGEgY8OzIHRo4buDIHLDunQgcmEsIHbDrSBk4bulLCBt4buZdCBz4buRIGluc2lnaHRzIHbhu4EgaMOgbmggdmkgdGnDqnUgZMO5bmcveGVtIHBoaW0gY+G7p2EgY8OhYyB1c2VyczogDQoNCmBgYHtyfQ0KDQojIE1vc3QgcG9wdWxhciBnZW5yZXM6IA0KDQpkZl9nZW5yZXMwMSAlPiUgDQogIGdhdGhlcihHZW5yZSwgbikgJT4lIA0KICBncm91cF9ieShHZW5yZSkgJT4lIA0KICBzdW1tYXJpc2UoRnJlcSA9IHN1bShuKSkgJT4lIA0KICBhcnJhbmdlKEZyZXEpICU+JSANCiAgbXV0YXRlKEdlbnJlID0gZmFjdG9yKEdlbnJlLCBsZXZlbHMgPSBHZW5yZSkpICU+JSANCiAgZ2dwbG90KGFlcyhHZW5yZSwgRnJlcSkpICsgDQogIGdlb21fY29sKCkgKyANCiAgY29vcmRfZmxpcCgpICsgDQogIGxhYnMoeCA9IE5VTEwsIHkgPSBOVUxMLCB0aXRsZSA9ICJGaWd1cmUgMTogTW9zdCBWaWV3ZWQgTW92aWVzIikNCg0KIyBHZW5yZSB0cmVuZDogDQoNCm1vdmllX2RhdGFfMDEgJT4lIA0KICBzZWxlY3QoLWMoMTo3KSkgJT4lIA0KICBnYXRoZXIoZ2VucmVzLCBjb3VudCwgLWRhdGVfeW1kKSAlPiUgDQogIGdyb3VwX2J5KGRhdGVfeW1kLCBnZW5yZXMpICU+JSANCiAgc3VtbWFyaXNlKHRvdGFsID0gc3VtKGNvdW50KSkgJT4lIA0KICBnZ3Bsb3QoYWVzKGRhdGVfeW1kLCB0b3RhbCwgY29sb3IgPSBnZW5yZXMpKSArIA0KICBnZW9tX2xpbmUoc2hvdy5sZWdlbmQgPSBGQUxTRSkgKyANCiAgZmFjZXRfd3JhcCh+IGdlbnJlcywgc2NhbGVzID0gImZyZWVfeCIpICsgDQogIGxhYnMoeCA9IE5VTEwsIHkgPSBOVUxMLCB0aXRsZSA9ICJGaWd1cmUgMjogVHJlbmQgb2YgVmlld2VkIE1vdmllcyIpDQoNCg0KIyBNb3N0IHdhdGNoZWQgbW92aWVzICh0b3AgMTApOiANCg0KbW92aWVfZGF0YV8wMSAlPiUgDQogIGdyb3VwX2J5KHRpdGxlKSAlPiUgDQogIGNvdW50KCkgJT4lIA0KICBhcnJhbmdlKC1uKSAlPiUgDQogIGhlYWQoMTApICU+JSANCiAga2FibGUoY2FwdGlvbiA9ICJUYWJsZSAzOiBUb3AtMTAgTW92aWVzIikNCmBgYA0KDQpLaMO0bmcgbuG6sW0gbmdvw6BpIGThu7EgxJFvw6FuLCBTaGF3c2hhbmsgUmVkZW1wdGlvbiAoYuG7mSBwaGltIMawYSB0aMOtY2ggY+G7p2EgbmfGsOG7nWkgdmnhur90IGLDoGkgbsOgeSkgY8OzIHPhu5EgbMaw4bujdCB4ZW0geOG6v3AgdGjhu6kgaGFpIGNo4buJIHNhdSBwaGltIGTDoG5oIGdp4bqjaSBPc2NhciBuxINtIDE5OTQgRm9ycmVzdCBHdW1wLiBDaMO6bmcgdGEgdMOsbSBoaeG7g3UgdGjDqm0gduG7gSBt4buZdCBz4buRIHRow7RuZyB0aW4gY8OzIHRo4buDIGzDoCBo4buvdSDDrWNoIHRodSDEkcaw4bujYyB04burIGLhu5kgZOG7ryBsaeG7h3UuIFbDrSBk4bulOiB04buVbmcgc+G7kSBsxrDhu6N0IG5nxrDhu51pIHhlbSBwaGltIGzDoCBgciBucm93KG1vdmllX2RhdGFfMDEpYCBjaG8gdOG6pXQgY+G6oyBgciBtb3ZpZV9kYXRhXzAxJG1vdmllSWQgJT4lIG5fZGlzdGluY3QoKWAgYuG7mSBwaGltLCBuaMawIHbhuq15IGLDrG5oIHF1w6JuIG3hu5l0IGLhu5kgcGhpbSDEkcaw4bujYyB4ZW0gY2jhu6tuZyAxMC4zNyBs4bqnbiBuaMawbmcgcGjDom4gYuG7kSBy4bqldCBs4buHY2guIFRo4buxYyB24bqteSwgY2jhu4kgY8OzIDQ1MCBi4buZIHBoaW0gY8OzIHPhu5EgbMaw4bujdCB4ZW0gbmhp4buBdSBoxqFuIDUwOiANCg0KYGBge3J9DQoNCm1vdmllX2RhdGFfMDEgJT4lIA0KICBncm91cF9ieSh0aXRsZSkgJT4lIA0KICBjb3VudCgpICU+JSANCiAgZmlsdGVyKG4gPj0gNTApICU+JSANCiAgYXJyYW5nZSgtbikgLT4gbW92aWVzX292ZXI1MF92aWV3ZWQNCg0KDQprYWJsZShtb3ZpZXNfb3ZlcjUwX3ZpZXdlZCAlPiUgaGVhZCgpLCBjYXB0aW9uID0gIlRhYmxlIDQ6IFRvcCA0NTAgTW92aWVzIikNCg0KYGBgDQoNCkZpZ3VyZSAzIGNobyB0aOG6pXkgcmF0aW5nID0gNCBsw6AgeOG6v3AgaOG6oW5nIHBo4buVIGJp4bq/biBuaOG6pXQgY+G7p2EgbmfGsOG7nWkgZMO5bmcgZMOgbmggY2hvIGPDoWMgYuG7mSBwaGltOiANCg0KYGBge3J9DQptb3ZpZV9kYXRhXzAxICU+JSANCiAgZ3JvdXBfYnkocmF0aW5nKSAlPiUgDQogIGNvdW50KCkgJT4lIA0KICB1bmdyb3VwKCkgJT4lIA0KICBtdXRhdGUocmF0aW5nID0gYXMuZmFjdG9yKHJhdGluZykpICU+JSANCiAgZ2dwbG90KGFlcyhyYXRpbmcsIG4pKSArIA0KICBnZW9tX2NvbCgpICsgDQogIGxhYnMoeCA9IE5VTEwsIHkgPSBOVUxMLCB0aXRsZSA9ICJGaWd1cmUgMzogRGlzdHJpYnV0aW9uIG9mIFJhdGluZyIpDQpgYGANCg0KDQojIFJlY29tbWVuZGVyIGJhc2VkIG9uIFJhdGluZ3M6IEl0ZW0tYmFzZWQgQ0ZSDQoNCkNGUiBjw7MgdGjhu4MgxJHGsOG7o2MgeMOieSBk4buxbmcgZOG7sWEgdsOgbyB44bq/cCBo4bqhbmcgKHJhdGluZ3MpIG3DoCB1c2VyIGLDrG5oIGLhuqd1IGNobyBi4buZIHBoaW0gaOG7jSDEkcOjIHhlbS4gTmjGsG5nIHRyxrDhu5tjIGjhur90IGNow7puZyB0YSBj4bqnbiBjb252ZXJ0IHjhur9wIGjhuqFuZyBj4bunYSBuaOG7r25nIGLhu5kgcGhpbSBi4bufaSB1c2VyIChuZ8aw4budaSB4ZW0pIHbhu4EgbWEgdHLhuq1uIHRoxrBhIChzcGFyc2UgbWF0cml4KTogDQoNCg0KYGBge3J9DQojIENvbnZlcnQgdG8gc3BhcnNlIG1hdHJpeCBmb3IgcmF0aW5nczogDQoNCnJhdGluZ3MgJT4lIA0KICBzZWxlY3QobW92aWVJZCwgdXNlcklkLCByYXRpbmcpICU+JSANCiAgc3ByZWFkKHZhbHVlID0gcmF0aW5nLCBrZXkgPSBtb3ZpZUlkKSAtPiBzcGFyc2VfZGYNCg0KYGBgDQoNClbDrCBjw7MgNjEwIHVzZXJzIHbDoCA5NzI0IGLhu5kgcGhpbSBuw6puIHNwYXJzZSBtYXRyaXggc+G6vSBuw6puIGzDoCBt4buZdCBtYSB0cuG6rW4gY8OzIDYxMCBkw7JuZyB2w6AgOTcyNCBj4buZdC4gS+G6v3QgcXXhuqMgc2F1IMSRw6J5IGNobyB0aOG6pXkgc3BhcnNlIGRhdGEgZnJhbWUgZMawIHJhIG3hu5l0IGPhu5l0IGzDoCB1c2VySWQgKGPDoWMgY+G7mXQgYmnhur9uIGtow6FjIGNow61uaCBsw6AgbW92aWVJZCAtIGhheSBsw6AgY8OhYyBpdGVtcyk6IA0KDQoNCmBgYHtyfQ0KaGVhZChzcGFyc2VfZGZbMTo2LCAxOjZdKQ0KYGBgDQoNCg0KRG8gduG6rXkgY2jDum5nIHRhIGPhuqduIGxv4bqhaSBi4buPIGPhu5l0IGJp4bq/biB1c2VySWQgbsOgeSB0csaw4bubYyBraGkgY29udmVydCB24buBIHNwYXJzZSBtYXRyaXggYuG6sW5nIGjDoG0gYGFzKClgIG5oxrAgc2F1OiANCg0KDQpgYGB7cn0NCg0KIyBSZW1vdmUgdXNlcklkIGFuZCBjb252ZXJ0IHRvIG1hdHJpeDogDQoNCnNwYXJzZV9kZiAlPiUgDQogIHNlbGVjdCgtdXNlcklkKSAlPiUgDQogIGFzLm1hdHJpeCgpIC0+IHJhdGluZ21hdA0KDQojIENvbnZlcnQgdG8gcmVhbFJhdGluZ01hdHJpeCBjbGFzcyBmb3IgbW9kZWxpbmcgUmVjb21tZW5kYXRpb24gU3lzdGVtOiANCg0KbGlicmFyeShyZWNvbW1lbmRlcmxhYikNCg0KYXMocmF0aW5nbWF0LCAicmVhbFJhdGluZ01hdHJpeCIpIC0+IHJhdGluZ21hdA0KDQpgYGANCg0KDQpDRlIgduG6rW4gaMOgbmggZOG7sWEgdHLDqm4gdGjGsOG7m2MgxJFvIGfhu41pIGzDoCBz4buxIHTGsMahbmcgdOG7sSAoc2ltaWxhcml0eSkgaGF5ICJraG/huqNuZyBjw6FjaCIgZ2nhu69hIGPDoWM6ICgxKSB1c2VycywgKDIpIGhv4bq3YyBpdGVtcyB2w6AgxJHGsOG7o2MgdMOtbmggdG/DoW4gdGhlbyBiYSBjw6FjaCBraMOhYyBuaGF1OiAoMSkgY29zaW5lIHNpbWlsYXJpdHksICgyKSBQZWFyc29uIHNpbWlsYXJpdHksIHbDoCAoMykgSmFjY2FyZCBzaW1pbGFyaXR5LiBUaMaw4bubYyDEkW8gbsOgeSDEkcaw4bujYyB0w61uaCB0b8OhbiBi4bqxbmcgY8OhY2ggc+G7rSBk4bulbmcgaMOgbSBgc2ltaWxhcml0eSgpYCBj4bunYSB0aMawIHZp4buHbiByZWNvbW1lbmRlcmxhYiBuaMawIHNhdTogDQoNCmBgYHtyfQ0KDQojIENhbGN1bGF0ZSBjb3NpbmUgc2ltaWxhcml0eSBmb3IgMTAgb2JzZXJ2YXRpb24gYnkgdXNlcjogDQoNCnNpbWlsYXJpdHlfdXNlcnMgPC0gc2ltaWxhcml0eShyYXRpbmdtYXRbMToxMCwgXSwgbWV0aG9kID0gImNvc2luZSIsIHdoaWNoID0gInVzZXJzIikNCg0KIyBQbG90IGNvc2luZSBzaW1pbGFyaXR5IGhlYXRtYXA6IA0KDQppbWFnZShhcy5tYXRyaXgoc2ltaWxhcml0eV91c2VycyksIG1haW4gPSAiRmlndXJlIDQ6IFNpbWlsYXJpdHkgYnkgVXNlciIpDQpgYGANCg0KSG/hurdjIHRoZW0gaXRlbXM6IA0KDQpgYGB7cn0NCiMgQ2FsY3VsYXRlIGNvc2luZSBzaW1pbGFyaXR5IGZvciAxMCBvYnNlcnZhdGlvbiBieSBpdGVtOiANCg0Kc2ltaWxhcml0eV9pdGVtcyA8LSBzaW1pbGFyaXR5KHJhdGluZ21hdFssIDE6MTBdLCBtZXRob2QgPSAiY29zaW5lIiwgd2hpY2ggPSAiaXRlbXMiKQ0KDQppbWFnZShhcy5tYXRyaXgoc2ltaWxhcml0eV9pdGVtcyksIG1haW4gPSAiRmlndXJlIDU6IFNpbWlsYXJpdHkgYnkgSXRlbSIpDQpgYGANCg0KVMawxqFuZyB04buxIG5oxrAgduG6rXkgbMOgIGhlYXRtYXAgY2hvIHJhdGluZyBtw6Ag4bufIMSRw7MgbeG7l2kgbeG7mXQgZMOybmcgbMOgIG3hu5l0IHVzZXIgY8OybiBt4buXaSBt4buZdCBj4buZdCBsw6AgbW92aWUuIFbDrSBk4bulLCAyMCB1c2VycyArIDIwIG1vdmllcyDEkeG6p3UgdGnDqm46IA0KDQpgYGB7cn0NCmltYWdlKHJhdGluZ21hdFsxOjIwLCAxOjIwXSwgbWFpbiA9ICJGaWd1cmUgNjogSGVhdG1hcCBvZiB0aGUgZmlyc3QgMjAgcm93cyBhbmQgMjAgY29sdW1ucyIpDQpgYGANCg0KDQpT4butIGThu6VuZyA4MCUgZGF0YSDEkeG7gyB0cmFpbiBDRlIgdsOgIHBo4bqnbiBk4buvIGxp4buHdSBjw7JuIGzhuqFpIMSR4buDIHRlc3QgQ0ZSOiANCg0KYGBge3J9DQpzZXQuc2VlZCgxKQ0KaWQgPC0gc2FtcGxlKHggPSAxOm5yb3cocmF0aW5nbWF0KSwgc2l6ZSA9IDAuOCpucm93KHJhdGluZ21hdCksIHJlcGxhY2UgPSBGQUxTRSkNCmRhdGFfdHJhaW4gPC0gcmF0aW5nbWF0W2lkLCBdDQpkYXRhX3Rlc3QgPC0gcmF0aW5nbWF0Wy1pZCwgXQ0KYGBgDQoNCkh14bqlbiBsdXnhu4duIENGUiBFbmdpbmU6IA0KDQpgYGB7cn0NCg0KIyBEZWZpbmUgc29tZSBwYXJhbWV0ZXJzIGZvciBDRlIgRW5naW5lOiANCg0KbXlfcGFyYW1zIDwtIGxpc3QoayA9IDMwLCBtZXRob2QgPSAiQ29zaW5lIikNCg0KIyBUcmFpbiBDRlIgcmVjb21tZW5kZXI6IA0KDQpjZnJfcmVjb21tZW5kZXIgPC0gUmVjb21tZW5kZXIoZGF0YSA9IGRhdGFfdHJhaW4sIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG1ldGhvZCA9ICJJQkNGIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBwYXJhbWV0ZXIgPSBteV9wYXJhbXMpDQoNCmBgYA0KDQpT4butIGThu6VuZyBDRlIgRW5naW5lIMSRw6MgY8OzIMSR4buDIMSRxrBhIHJhIHJlY29tbWVuZGF0aW9ucyBjaG8gY8OhYyB1c2VyczogDQoNCmBgYHtyfQ0KIyBVc2UgQ0ZSIHJlY29tbWVuZGVyIGZvciB0ZXN0IGRhdGEuIA0KDQpuX3JlY29tbWVuZGVkIDwtIDMNCg0KaXRlbXNfcHJlZGljdGVkIDwtIHByZWRpY3Qob2JqZWN0ID0gY2ZyX3JlY29tbWVuZGVyLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgIG5ld2RhdGEgPSBkYXRhX3Rlc3QsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgbiA9IG5fcmVjb21tZW5kZWQpDQpgYGANCg0KQ2jDum5nIHRhIGPFqW5nIGPDsyB0aOG7gyBsaXN0IGRhbmggc8OhY2ggMjAgYuG7mSBwaGltIMSRxrDhu6NjIMSR4buBIHh14bqldCBuaGnhu4F1IG5o4bqldCBi4bufaSBDRlIgRW5naW5lIChOaXhvbiAoMTk5NSkgbMOgIGLhu5kgcGhpbSDEkcaw4bujYyBraHV54bq/biBuZ2jhu4sgbmhp4buBdSBs4bqnbiBuaOG6pXQgYuG7n2kgQ0ZSIEVuZ2luZSB24bubaSAxMCBs4bqnbik6IA0KDQpgYGB7cn0NCmRhdGEuZnJhbWUobW92aWVJZCA9IHVubGlzdChpdGVtc19wcmVkaWN0ZWRAaXRlbXMpKSAlPiUgDQogIGdyb3VwX2J5KG1vdmllSWQpICU+JSANCiAgY291bnQoKSAlPiUgDQogIHVuZ3JvdXAoKSAlPiUgDQogIHRvcF9uKG4gPSAyMCwgd3QgPSBuKSAlPiUgDQogIGFycmFuZ2UobikgJT4lIA0KICBpbm5lcl9qb2luKG1vdmllc19jb21tb24gJT4lIHNlbGVjdCgxOjIpKSAlPiUgDQogIG11dGF0ZSh0aXRsZSA9IGZhY3Rvcih0aXRsZSwgbGV2ZWxzID0gdGl0bGUpKSAtPiB0b3BfMjBfcmVjb21tZW5kZWQNCg0KdG9wXzIwX3JlY29tbWVuZGVkICU+JSANCiAgZ2dwbG90KGFlcyh0aXRsZSwgbikpICsgDQogIGdlb21fY29sKCkgKyANCiAgZ2VvbV90ZXh0KGFlcyhsYWJlbCA9IG4pLCBoanVzdCA9IDEuMywgY29sb3IgPSAid2hpdGUiKSArIA0KICBjb29yZF9mbGlwKCkgKyANCiAgbGFicyh4ID0gTlVMTCwgeSA9IE5VTEwsIHRpdGxlID0gIkZpZ3VyZSA3OiBUb3AtcmVjb21tZW5kZWQgTW92aWVzIGJ5IENGUiIpDQoNCiAgDQpgYGANCg0KWGVtIDMgYuG7mSBwaGltIG3DoCBFbmdpbmUga2h1eeG6v24gbmdo4buLIGNobywgdsOtIGThu6UsIHVzZXIgdGjhu6kgbmjhuqV0ICh0cm9uZyB0ZXN0IGRhdGEpOiANCg0KYGBge3J9DQptb3ZpZXNfcmVjb21tZW5kZWRfdXNlcjEgPC0gaXRlbXNfcHJlZGljdGVkQGl0ZW1zW1sxXV0NCg0KbW92aWVzX2NvbW1vbiAlPiUgDQogIGZpbHRlcihtb3ZpZUlkICVpbiUgbW92aWVzX3JlY29tbWVuZGVkX3VzZXIxKSAlPiUgDQogIHNlbGVjdChtb3ZpZUlkLCB0aXRsZSkNCg0KYGBgDQoNCiMgRmluYWwgTm90ZXMNCg0KUG9zdCBuw6B5IHRyw6xuaCB04bqtcCB0cnVuZyB2w6BvIHZp4buHYyB0aOG7sWMgaGnhu4duIHRp4buBbiB44butIGzDrSBz4buRIGxp4buHdSAtIGNodeG6qW4gYuG7iyBz4buRIGxp4buHdSBjaG8gxJHhur9uIHZp4buHYyB4w6J5IGThu7FuZyBt4buZdCBo4buHIHRo4buRbmcga2h1eeG6v24gbmdo4buLIGLhurFuZyBuZ8O0biBuZ+G7ryBSIG3DoCBj4bulIHRo4buDIGzDoCBDb2xsYWJvcmF0aXZlIEZpbHRlcmluZyBSZWNvbW1lbmRlci4gTeG7mXQgc+G7kSB24bqlbiDEkeG7gSBxdWFuIHRy4buNbmcgduG6q24gY2jGsGEgxJHGsOG7o2MgxJHhu4EgY+G6rXAvZ2nhuqNpIHF1eeG6v3QgbMOgOiANCg0KMS4gxJDDoW5oIGdpw6EgY2jhuqV0IGzGsOG7o25nIGPhu6dhIFJlY29tbWVuZGVyIEVuZ2luZS4gVsOtIGThu6UsIHPhu60gZOG7pW5nIENGUiBFbmdpbmUgxJHDoyBjw7MgY2jDum5nIHRhIMSRxrBhIHJhIGtodXnhur9uIGPDoW8gY+G7pSB0aOG7gyBiYSBi4buZIHBoaW0gbcOgIHVzZXIgdGjhu6kgbmjhuqV0IG7Dqm4geGVtLiBOaMawbmcgbMOgbSB0aOG6vyBuw6BvIMSR4buDIMSRw6FuaCBnacOhIGtodXnhur9uIG5naOG7iyBuw6B5IGPDsyBraOG7m3AgduG7m2kgbmh1IGPhuqd1IHbDoCBz4bufIHRow61jaCBj4bunYSBuZ8aw4budaSBuw6B5IGhheSBraMO0bmc/IA0KDQoyLiBN4buZdCBz4buRIGPDoWNoIHRp4bq/cCBj4bqtbiBraMOhYyBjaG8geMOieSBk4buxbmcgUmVjb21tZW5kZXIgRW5naW5lIGPFqW5nIG5oxrAgdGluaCBjaOG7iW5oIHRoYW0gc+G7kS4gDQoNCjMuIENo4bqldCBsxrDhu6NuZyBj4bunYSBFbmdpbmUgY8OybiBwaOG7pSB0aHXhu5ljIHLhuqV0IG5oaeG7gXUgbOG7sWEgY2jhu41uIGPDoWMgb2JzZXJ2YXRpb25zIHBow7kgaOG7o3AuIFbDrSBk4bulOiBt4buZdCBtw7QgaMOsbmggZOG7sWEgdHLDqm4gaMOgbmggdmkgY+G7p2EgdXNlciB0aMOsIHF1YW4gc8OhdCB24buBIHVzZXIgcGjhuqNpIMSR4bunIG5oaeG7gXUgxJHhu4MgZW5naW5lIGPDsyB0aOG7gyAidHLDrWNoIHh14bqldCIgcmEgaW5zaWdodHMvcGF0dGVybiDEkeG6oWkgZGnhu4duIGNobyBow6BuaCB2aSB0acOqdSBkw7luZyAodHJvbmcgdMOsbmggaHXhu5FuZyBuw6B5IGzDoCB4ZW0gcGhpbSkuIENGUiBFbmdpbmUgxJHGsOG7o2MgeMOieSBk4buxbmcgdHJvbmcgcG9zdCBuw6B5IHPhu60gZOG7pW5nIGPhuqMgbmjhu69uZyB1c2VyIG3DoCBo4buNIGNo4buJIHhlbSBt4buZdCB2w6BpIHBoaW0uIE5o4buvbmcgdXNlciBuw6B5IHF1w6Egw610IHTGsMahbmcgdMOhYyB2w6AgZG8gduG6rXkgaMOgbmggdmkgY+G7p2EgaOG7jSBraMO0bmcgcsO1IHLDoG5nIChob+G6t2Mga2jDtG5nIGPDsykgdsOsIHRo4bq/IHPhu60gZOG7pW5nIGPhuqMgbmjhu69uZyB1c2VyIG7DoHkga2hpIHjDonkgZOG7sW5nIFJlY29tbWVuZGVyIGPDsyB0aOG7gyBraMO0bmcgcGjDuSBo4bujcC4gDQoNCjQuIE7hur91IGPDsyBz4bq1biBk4buvIGxp4buHdSB24buBIHJhdGluZ3MgKGzDoCB0w6xuaCBodeG7kW5nIGPhu6dhIGLhu5kgZOG7ryBsaeG7h3Ugc+G7rSBk4bulbmcgdHJvbmcgcG9zdCBuw6B5KSB0aMOsIHZp4buHYyB4w6J5IGThu7FuZyBSZWNvbW1lbmRlciBFbmdpbmUgbMOgIHRodeG6rW4gdGnhu4duLiBUdXkgbmhpw6puIHRyb25nIHRo4buxYyB04bq/IHRow6wga2jDtG5nIHBo4bqjaSBsw7pjIG7DoG8gY8WpbmcgY8OzIGThu68gbGnhu4d1IHbhu4EgcmF0aW5nIHbDoCB4w6J5IGThu7FuZyBt4buZdCBSZWNvbW1lbmRlciBFbmdpbmUgbMO6YyBuw6B5IHRy4bufIG7Dqm4ga2jDsyBraMSDbiBoxqFuLiANCg0KIyBSZWZlcmVuY2VzDQoNCjEuIFtCdWlsZGluZyBSZWNvbW1lbmRlciBTeXN0ZW1zIHdpdGggTWFjaGluZSBMZWFybmluZyBhbmQgQUldKGh0dHBzOi8vd3d3LmFtYXpvbi5jb20vQnVpbGRpbmctUmVjb21tZW5kZXItU3lzdGVtcy1NYWNoaW5lLUxlYXJuaW5nL2RwLzE3MTgxMjAxMjUvcmVmPXNyXzFfNT9jcmlkPTNKSFUxRk5aODBNTSZkY2hpbGQ9MSZrZXl3b3Jkcz1yZWNvbW1lbmRhdGlvbitzeXN0ZW0mcWlkPTE1OTM1OTg5MDQmc3ByZWZpeD1yZWNvbW1lbmRhdGlvbitzeXMlMkNhcHMlMkMzODQmc3I9OC01KS4gDQoNCjIuIFtCdWlsZGluZyBhIFJlY29tbWVuZGF0aW9uIFN5c3RlbSB3aXRoIFJdKGh0dHBzOi8vd3d3LmFtYXpvbi5jb20vQnVpbGRpbmctUmVjb21tZW5kYXRpb24tU3lzdGVtLVN1cmVzaC1Hb3Jha2FsYS1lYm9vay9kcC9CMDEyTzhTMVlNL3JlZj1zcl8xXzY/Y3JpZD0zSkhVMUZOWjgwTU0mZGNoaWxkPTEma2V5d29yZHM9cmVjb21tZW5kYXRpb24rc3lzdGVtJnFpZD0xNTkzNTk4OTA0JnNwcmVmaXg9cmVjb21tZW5kYXRpb24rc3lzJTJDYXBzJTJDMzg0JnNyPTgtNikuIA0KDQozLiBbUHJhY3RpY2FsIFJlY29tbWVuZGVyIFN5c3RlbXNdKGh0dHBzOi8vd3d3LmFtYXpvbi5jb20vUHJhY3RpY2FsLVJlY29tbWVuZGVyLVN5c3RlbXMtS2ltLUZhbGsvZHAvMTYxNzI5MjcwMi9yZWY9c3JfMV80P2NyaWQ9M0pIVTFGTlo4ME1NJmRjaGlsZD0xJmtleXdvcmRzPXJlY29tbWVuZGF0aW9uK3N5c3RlbSZxaWQ9MTU5MzU5ODkwNCZzcHJlZml4PXJlY29tbWVuZGF0aW9uK3N5cyUyQ2FwcyUyQzM4NCZzcj04LTQpLiANCg0KNC4gW0hhbmRzLU9uIFJlY29tbWVuZGF0aW9uIFN5c3RlbXMgd2l0aCBQeXRob246IFN0YXJ0IGJ1aWxkaW5nIHBvd2VyZnVsIGFuZCBwZXJzb25hbGl6ZWQsIHJlY29tbWVuZGF0aW9uIGVuZ2luZXMgd2l0aCBQeXRob25dKGh0dHBzOi8vd3d3LmFtYXpvbi5jb20vSGFuZHMtUmVjb21tZW5kYXRpb24tU3lzdGVtcy1QeXRob24tcmVjb21tZW5kYXRpb24vZHAvMTc4ODk5Mzc1Ni9yZWY9c3JfMV8zP2NyaWQ9M0pIVTFGTlo4ME1NJmRjaGlsZD0xJmtleXdvcmRzPXJlY29tbWVuZGF0aW9uK3N5c3RlbSZxaWQ9MTU5MzU5ODkwNCZzcHJlZml4PXJlY29tbWVuZGF0aW9uK3N5cyUyQ2FwcyUyQzM4NCZzcj04LTMpLiANCg0KNS4gW0V2YWx1YXRpb24gTWV0cmljcyBmb3IgUmVjb21tZW5kZXIgU3lzdGVtc10oaHR0cHM6Ly90b3dhcmRzZGF0YXNjaWVuY2UuY29tL2V2YWx1YXRpb24tbWV0cmljcy1mb3ItcmVjb21tZW5kZXItc3lzdGVtcy1kZjU2YzY2MTEwOTMpLiANCg0KNi4gW0V2YWx1YXRpbmcgUmVjb21tZW5kYXRpb24gU3lzdGVtc10oaHR0cDovL3d3dy5iZ3UuYWMuaWwvfnNoYW5pZ3UvUHVibGljYXRpb25zL0V2YWx1YXRpb25NZXRyaWNzLjE3LnBkZikuIA0KDQo3LiBbQSBTdXJ2ZXkgb2YgQWNjdXJhY3kgRXZhbHVhdGlvbiBNZXRyaWNzIG9mIFJlY29tbWVuZGF0aW9uIFRhc2tzXShodHRwOi8vam1sci5jc2FpbC5taXQuZWR1L3BhcGVycy92b2x1bWUxMC9ndW5hd2FyZGFuYTA5YS9ndW5hd2FyZGFuYTA5YS5wZGYpLiANCg0KDQoNCg0KDQoNCg0KDQo=