(1) Document and Term Matrix (DTM)
先做兩個 Document-Term Matrix …
dtm1 : 完整字詞、未還原字根
corp = Corpus(VectorSource(review$text))
corp = tm_map(corp, content_transformer(tolower))
corp = tm_map(corp, removePunctuation)
dtm1 = DocumentTermMatrix(corp); dtm1 # terms: 215402
<<DocumentTermMatrix (documents: 215879, terms: 215402)>>
Non-/sparse entries: 15740971/46485027387
Sparsity : 100%
Maximal term length: 932
Weighting : term frequency (tf)
dtm1 = removeSparseTerms(dtm1, .9999); dtm1 # terms: 18664
<<DocumentTermMatrix (documents: 215879, terms: 18664)>>
Non-/sparse entries: 15289544/4013876112
Sparsity : 100%
Maximal term length: 18
Weighting : term frequency (tf)
dtm2 : 字根還原
corp = tm_map(corp, removeWords, stopwords("english"))
corp = tm_map(corp, stemDocument)
dtm2 = DocumentTermMatrix(corp); dtm2 # terms: 177911
<<DocumentTermMatrix (documents: 215879, terms: 177911)>>
Non-/sparse entries: 11655667/38395593102
Sparsity : 100%
Maximal term length: 932
Weighting : term frequency (tf)
dtm2 = removeSparseTerms(dtm2, .9999); dtm2 # terms: 12968
<<DocumentTermMatrix (documents: 215879, terms: 12968)>>
Non-/sparse entries: 11312194/2788206678
Sparsity : 100%
Maximal term length: 17
Weighting : term frequency (tf)
save(dtm1,dtm2,file='data/dtm.rdata')
先存起來,免得每次都要重做。
(2) Preparation
load('data/dtm.rdata')
DTM = dtm1 %>% {.[, order(-col_sums(.))]}
為了觀察方便,我們選擇完整字詞(未還原字根)的DTM,讀進DTM之後,通常先把字詞欄位依字詞的出現評率排列。
Effect = function(y, x, m=rep(TRUE, length(y))) {
x = x[m]; y = y[m]
n = as.numeric(length(x))
pX = sum(x)/n; pY = sum(y)/n; pXY = sum(x&y)/n
ef = c(usage=pX, base=pY, support=pXY, conf=pXY/pX, lift=pXY/pX/pY)
c(round(100*ef, 2), count=n) }
定義一個function(Effect())來計算X對Y的各種效果,包括:
- Usage (
Pr[X]) – the usage ratio of X
- Base (
Pr[Y]) – the overall probability of Y
- Confidence (
Pr[Y|X]) – the probability of Y given X
- Support (
Pr[Y^X]) – the probability of X^Y
- Lift (
Pr[Y|X]/Pr[Y]) – X’s effect on Y (the lift of Y’s probability given X)
- Count – the length of the vectors (
X and Y should have the same length)
Y = review[,"useful"] %>% {. > median(.)}
Effect(Y, as.vector(DTM[,"pizza"]) > 0)
usage base support conf lift count
6.18 30.08 1.89 30.54 101.55 215879.00
pizza has no effect on review$useful (its lift is barely higher than 100%.)
Effect(Y, as.vector(DTM[,"dresses"]) > 0)
usage base support conf lift count
0.20 30.08 0.09 47.07 156.50 215879.00
dresses has a positive effect on review$useful (its lift is higher than 150%.)
2.1 – The effect of 20 most frequent words
Now we can observe the effect of the most frequent (20) words …
df = t(sapply(colnames(DTM)[1:20], function(w)
Effect(Y, as.vector(DTM[, w]) > 0)))
df
usage base support conf lift count
the 91.48 30.08 28.99 31.69 105.36 215879
and 89.02 30.08 28.33 31.82 105.80 215879
was 56.59 30.08 19.63 34.69 115.35 215879
for 65.38 30.08 22.75 34.80 115.71 215879
that 51.60 30.08 19.56 37.91 126.05 215879
with 52.02 30.08 19.22 36.94 122.82 215879
but 54.98 30.08 19.55 35.56 118.21 215879
this 56.42 30.08 19.91 35.28 117.30 215879
you 44.47 30.08 16.71 37.59 124.96 215879
they 46.53 30.08 16.65 35.79 118.98 215879
have 48.28 30.08 17.46 36.16 120.22 215879
not 42.80 30.08 15.75 36.81 122.37 215879
had 40.13 30.08 14.31 35.67 118.59 215879
are 40.38 30.08 14.26 35.31 117.39 215879
good 41.73 30.08 13.67 32.75 108.87 215879
place 42.74 30.08 14.40 33.68 111.99 215879
were 31.79 30.08 11.86 37.31 124.04 215879
food 38.89 30.08 12.28 31.57 104.97 215879
there 33.28 30.08 12.55 37.71 125.36 215879
great 35.08 30.08 10.43 29.75 98.90 215879
As you have just experienced, calculating the effect is a lengthy task. Let’s turn on parallel computation.
library(doParallel)
K = 4; clust=makeCluster(K)
registerDoParallel(clust)
getDoParWorkers()
[1] 4
(3) Words’ Effect on the Entire Corpus
3.1 – The Most Frequent Words
Pick the most frequent works (360) from the DTM. Since we had sorted DTM by descending col_sums(), we simply take the first 360 colnames() from it. It’d take some time even with parallel processing (~60 seconds in my notebook). So, be patient.
Words = colnames(DTM)[1:360]
t0 = Sys.time()
df = foreach(word = Words, .combine=rbind) %dopar% {
library(slam)
Effect(Y, row_sums(DTM[,word]) > 0) }
Sys.time() - t0
Time difference of 1.0529 mins
df = data.frame(df, word=Words)
df[1:6,]
hchart(df,"scatter",hcaes(
y=usage,x=lift,color=conf,size=conf,group=word)) %>%
hc_legend(enabled=F) %>% hc_chart(zoomType="xy") %>%
hc_add_theme(hc_theme_flat()) %>%
hc_plotOptions(bubble=list(maxSize="2%",minSize=10)) %>%
hc_xAxis(plotLines=list(list(color="pink",value=mean(df$lift),width=2))) %>%
hc_title(text="Effect of the Most Frequent Words (360) on Usefulness")
In the figure above, each bubble represents a word. The bubbles’ …
- Colors represent the
confidence: Pr[Y|X]; yellow/blue indicates high/low confidence.
- The confidences are, by definition, closely correlated to the lift.
- X-coordinate represent the
lift: Pr[Y|X]/Pr[Y] in percentage.
- On the right, we can see the words of the highest lift, including:
yes, review, those, into, finally;
- One the left, we can see some words of low/negative lift.
- Interestingly, such words of positive implications as
great, excellent, recommend all carry negative lifts.
- On the upper left, we have the two most frequent words in English –
the and and.
- The positive bias of lift is quite obvious. Even such common words as
the and and carry positive lifts. To cope with this bias, we will adjust the neutral lift from 100 to 125 (the mean of the lift).
- Y-coordinate represent the
usage: Pr[X].
- Lift is negatively correlated with usage; all of the high-lift words exhibits low-usage.
3.2 – Select Words with TF-IDF
Usually, the most importance words are not the same as the most frequent words. We can use the TF-IDF (Term Frequency – Inverse Document Frequency) method to pick the important yet less frequent words.
tfidf = DTM %>% {tapply(.$v/row_sums(.)[.$i], .$j, mean) *
log2( nDocs(.) / col_sums(. > 0) )}
TFxIDF = DTM[,which(tfidf[1:3000] > quantile(tfidf)[3])] %>%
col_sums() %>% sort %>% names
length(TFxIDF)
[1] 377
The algorithm generate 377 words. To be consistent, we only take the first 360 words. We put the words in Words and use the same code to calculate the effects of these words …
Words = TFxIDF[1:360]
t0 = Sys.time()
df = foreach(word = Words, .combine=rbind) %dopar% {
library(slam)
Effect(Y, row_sums(DTM[,word]) > 0) }
Sys.time() - t0
Time difference of 56.508 secs
and then make a interactive chart.
df = data.frame(df, word=Words)
hchart(df,"scatter",hcaes(
y=usage,x=lift,color=conf,size=conf,group=word)) %>%
hc_legend(enabled=F) %>% hc_chart(zoomType="xy") %>%
hc_add_theme(hc_theme_flat()) %>%
hc_plotOptions(bubble=list(maxSize="2%",minSize=10)) %>%
hc_xAxis(plotLines=list(list(color="pink",value=mean(df$lift),width=2))) %>%
hc_title(text="Effect of TFxIDF Words (360-, censored) on Usefulness")
There are some major difference between the two charts:
- The Scale : The usages of TF-IDF words are lower, but their lifts are more diverse than the frequent words.
- Because TF-IDF rates the words by Inverse document frequency, such common words as
this and that are precluded. In this way, it helps to find the less frequent but important words.
- To aviod glitches, we censor the words with usage lower than 0.1%. The upper bound of usage are merely 1.33%, whilst that of frequent words is 91.48%.
- Although the usage of these words are low, the spread of their lifts are much wider – [75.9%, 205.8%] comparing to [98.6%, 153.4%].
The Distribution of Lift : The lifts exhibit a normal-alike-distribution. With the mean lifts is around 130%. Therfore it is easier to identify the ‘good’ and ‘bad’ words.
The Goods are: fez, gay, republic, lolos, hood, …
The Bads are: definately, haircut, auto, repair, gluten, …
(4) Words’ Effect Across Two Sub-Corpus
除了關心字詞在整個文集的表現,我們也可以比較字詞在不同種類的子文集之中的效果。
4.1 – Helper Function
一個字在不同子文集會有不同的lift,如果我們用這些lift做座標,把一整群字畫在同一個平面上,我們就可以比較字在子文集的效果。 我們先做一個製圖的helper function,叫它WordsLift().
WordsLift = function(L, On="Usefulness", c1="Cat1", c2="Cat2", Wd="Frequent") {
ttl = sprintf("Effect of %s Words on %s", Wd, On)
sub = sprintf("Size: Total Usage; Color: %s(yellow) | %s(blue)", c2, c1)
ttlx = sprintf("%% Lift on %s (blue)", c1)
ttly = sprintf("%% Lift on %s (yellow)", c2)
df = merge(L[[1]],L[[2]],by='word',sort=F,suffixes=c("_x","_y"))
df = df[df$usage_x > 0.1 & df$usage_y > 0.1, ]
df$ratio = round(df$usage_y / df$usage_x, 3)
df$total = round(df$usage_y + df$usage_x, 3)
tips=paste0("<b>{point.word}</b><br>",
"conf: ({point.conf_x}%, {point.conf_y}%)<br>",
"usage: ({point.usage_x}%, {point.usage_y}%)<br>",
"total uasge (y + x): {point.total}%<br>",
"uasge ratio (y / x): {point.ratio}")
hchart(df,"scatter",hcaes(
x=lift_x, y=lift_y, size=log(total), color=log(ratio))) %>%
hc_chart(zoomType="xy") %>% hc_add_theme(hc_theme_538()) %>%
hc_plotOptions(bubble=list(maxSize="2%",minSize=4)) %>%
hc_xAxis(title=list(text=ttlx)) %>% hc_yAxis(title=list(text=ttly)) %>%
hc_tooltip(headerFormat="",hideDelay=100,useHTML=T,pointFormat=tips) %>%
hc_xAxis(plotLines=list(list(color="orange",value=mean(df$lift_x),width=2))) %>%
hc_yAxis(plotLines=list(list(color="orange",value=mean(df$lift_y),width=2))) %>%
hc_title(text=ttl) %>% hc_subtitle(text=sub)
}
4.2 – Food vs. Non-Food, 360 Frequent Words
在CatGroup裡面定義兩個子文集:
Food: the Restaurants and Food categories
nonFood: others
把要分析的字放在Words裡面,然後用一個迴圈,對兩個子文集分別做效果分析,將結果放在L這個list裡面。
bids = rowSums(mxBC[,c('Restaurants','Food')]) > 0
CatGroup = list(Food=bids, nFood=!bids)
Words = colnames(DTM)[1:360]
L = list(); for(i in 1:length(CatGroup)) {
rmask = review$bid %in% biz$bid[ CatGroup[[i]] ]
df = foreach(word = Words, .combine=rbind) %dopar% {
Effect(Y, row_sums(DTM[,word]) > 0, rmask) }
df = data.frame(df, word=Words, catgrp=names(CatGroup)[i])
L[[i]] = df}
之後就可以用WordsLift()製圖。
# food.freq = L
WordsLift(food.freq, "Usefulness", "Food", "nonFood", "Frequent (360)")
圖中每一點代表一個字 …
- 點的橫(綜)座標代表字對
Food(nonFood)子集的lifts
- 點的大小代表字的使用率 (the sum of usage, log transfered)
- 點的顏色代表字出現在
Food(藍色)和`nonFood(黃色)子集的比重
We can observe that:
- As we’d observed in the previous charts, lift is negatively correlated with usage.
- Lift of
Food is positively correlated with nonFood
- In the upper left, some food related words (
pizza, tacos, sushi, chips, and spicy) are have low/high-lift on Food/nonFood.
4.3 – Food vs. Non-Food, 194 TF-IDF Words
Let’s repeat the process in the previous sub-session with TF-IDF words.
Words = TFxIDF[1:360]
L = list(); for(i in 1:length(CatGroup)) {
rmask = review$bid %in% biz$bid[ CatGroup[[i]] ]
df = foreach(word = Words, .combine=rbind) %dopar% {
Effect(Y, row_sums(DTM[,word]) > 0, rmask) }
df = data.frame(df, word=Words, catgrp=names(CatGroup)[i])
L[[i]] = df}
# food.tfidf = L
WordsLift(food.tfidf, "Usefulness", "Food", "nonFood",
"TFxIDF (360, censored by 0.1% usage)")
To improve the comparative validity, we censor the words with usage lower than 0.1% in either sub-corpus. Thereby, the words displayed are 174, instead of 360. As we can see …
- the distribution of lift of TF-IDF is wider than that of frequent words.
- The correlation between lift and usage remains
- But, the correlation between
Food and nonFood is no longer significant.
- By median split, we can divide the plane into four quarters. On the …
- upper right –
gay and republic is good for both sub-corpses
- lower left –
definately is bad for both sub-corpus
- lower right –
mike, polish, chris and result are good in Food but bad in nonFood
- lower left –
magaritas and peaks are good in nonFood but bad in Food
4.4 – Bars vs Shopping, 1000 Frequent Words
As an exercise, we compare the effect of
- the most frequent 1000 words
- on
review$cool
- across
Bars and Shopping categories
Simply put the criteria in Y, CatGroup and Words …
Y = review[,"cool"] %>% {. > median(.)}
CatGroup = list(Bar = mxBC[,'Bars'], Shopping = mxBC[,'Shopping'])
Words = colnames(DTM)[1:1000]
It take about 5 minutes to evaluate the effect of 1000 words.
t0 = Sys.time()
L = list(); for(i in 1:length(CatGroup)) {
rmask = review$bid %in% biz$bid[ CatGroup[[i]] ]
df = foreach(word = Words, .combine=rbind) %dopar% {
Effect(Y, row_sums(DTM[,word]) > 0, rmask) }
df = data.frame(df, word=Words, catgrp=names(CatGroup)[i])
L[[i]] = df}
Sys.time() - t0 # 4.7617 mins
Time difference of 4.8501 mins
We can plot the data with the same helper function (WordsLift())
# bars_shop = L
WordsLift(bars_shop, "Coolness", "Bars", "Shopping", "Frequent (1000)")
QUIZ :
- What are your observations in the chart above?
- Try to
- pick two groups categories
- make a lift comparison chart with the 360 most frequent words
- make a lift chart with the TF-IDF words
- share with us your major findings …
# save before leaving
save(bars_shop, food.freq, food.tfidf, file="data/wordeffect.rdata")
stopCluster(clust) # stop parallel processing
LS0tDQp0aXRsZTogIuWtl+ipnueahOaViOaenCINCnN1YnRpdGxlOiBZZWxwIEthZ2dsZSwgVGhlIEVmZmVjdCBvZiBXb3Jkcw0KYXV0aG9yOiAiVG9ueSBDaHVvLCB0b255Y2h1b0BnbWFpbC5jb20iDQpkYXRlOiAiMjAxNy8wOC8wNiINCm91dHB1dDoNCiAgaHRtbF9ub3RlYm9vazoNCiAgICBoaWdobGlnaHQ6IHRleHRtYXRlDQogICAgdGhlbWU6IGx1bWVuDQotLS0NCg0KPGJyPg0KPGJyPg0K5oiR5YCR5YWI5L6d5a2X6KmeKOaIluWtl+aguSnnmoQgLi4uDQoNCisg5Ye654++6KmV546HKGZyZXF1ZW5jZSkg5oiWDQorIOebuOWwjemHjeimgeaApyhhdmVyYWdlIHRmLWlkZiBzY29yZSkNCg0K6YG45a6a5LiA5om55a2X6Kme77yM54S25b6M6KeA5a+f5a6D5YCR5Zyo5pW05YCL5paH6ZuG5bCNYHJldmlldyR1c2VmdWxg44CBYHJldmlldyRjb29sYOaIlg0KYHJldmlldyRmdW5ueWDnmoTmlYjmnpzjgII8YnI+IDxicj4NCg0K5ZCM5pmC77yM5oiR5YCR5Lmf5Y+v5LulIC4uLg0KDQorIOS+neWVhualremhnuWIpe+8jOaKiuaVtOWAi+aWh+mbhuWIh+aIkOWFqemDqOWIhiAoZWc6IGBSZXN0YXVyYW50c2AgJiBOb24tYFJlc3RhdXJhbnRzYCkNCisg5Lu76YG45YWp5YCL5ZWG5qWt6aGe5YilIChlZzogYEJhcnNgJiBgU2hvcHBpbmdgKSDmiJYNCisg5Lu75oSP5a6a576p5YWp57WE5ZWG5qWt6aGe5YilIChlZzoge2BDaGluZXNlYCwgYEphcGVuZXNlYH0gJiB7YE1leGljYW5gLCBgVGV4LU1leGB9KQ0KDQrnhLblvozmr5TovIPpgJnkupvlrZfoqZ7lnKjlhanlgIvlrZDmlofpm4boo6HpnaLnmoTmlYjmnpzjgIINCg0KLSAtIC0NCg0KYGBge3Igc2V0LW9wdGlvbnMsIGVjaG89RkFMU0UsIGNhY2hlPUZBTFNFfQ0KbGlicmFyeShrbml0cikNCm9wdGlvbnMod2lkdGg9OTApDQpvcHRzX2NodW5rJHNldChjb21tZW50ID0gTkEpDQpgYGANCg0KDQpgYGB7ciByZXN1bHRzPSdhc2lzJywgd2FybmluZz1GLCBtZXNzYWdlPUYsIGNhY2hlPUZ9DQpTeXMuc2V0bG9jYWxlKCdMQ19BTEwnLCdDJykNCmxpYnJhcnkobWFncml0dHIpDQpsaWJyYXJ5KGhpZ2hjaGFydGVyKQ0KbGlicmFyeShzbGFtKQ0KbGlicmFyeSh0bSkNCmxpYnJhcnkoU25vd2JhbGxDKQ0KDQojIGdldCBjb2xvciBwYWxldHRlDQpsaWJyYXJ5KFJDb2xvckJyZXdlcikgICAgICAgICAgDQpwYWxzID0gYyhicmV3ZXIucGFsKDgsIlNldDIiKVtjKDYpXSwNCiAgICAgICAgIGJyZXdlci5wYWwoOCwiRGFyazIiKSwNCiAgICAgICAgIGJyZXdlci5wYWwoOCwiU2V0MSIpW2MoMSldKQ0KDQojIGxvYWQgZGF0YQ0KbG9hZCgnZGF0YS95ZWxwMS5yZGF0YScpDQpsb2FkKCdkYXRhL2F2ZXJhZ2UucmRhdGEnKQ0KbG9hZCgnZGF0YS9lbXBhdGgucmRhdGEnKQ0KYGBgDQo8YnI+DQoNCiMjICgxKSBEb2N1bWVudCBhbmQgVGVybSBNYXRyaXggKERUTSkNCuWFiOWBmuWFqeWAiyBEb2N1bWVudC1UZXJtIE1hdHJpeCAuLi4NCg0KIyMjICBgZHRtMWAgOiDlrozmlbTlrZfoqZ7jgIHmnKrpgoTljp/lrZfmoLkNCmBgYHtyfQ0KY29ycCA9IENvcnB1cyhWZWN0b3JTb3VyY2UocmV2aWV3JHRleHQpKQ0KY29ycCA9IHRtX21hcChjb3JwLCAgY29udGVudF90cmFuc2Zvcm1lcih0b2xvd2VyKSkNCmNvcnAgPSB0bV9tYXAoY29ycCwgcmVtb3ZlUHVuY3R1YXRpb24pDQpkdG0xID0gRG9jdW1lbnRUZXJtTWF0cml4KGNvcnApOyBkdG0xICAgICAgICAjIHRlcm1zOiAyMTU0MDINCmR0bTEgPSByZW1vdmVTcGFyc2VUZXJtcyhkdG0xLCAuOTk5OSk7IGR0bTEgICMgdGVybXM6IDE4NjY0DQpgYGANCjxicj4NCg0KIyMjICBgZHRtMmAgOiDlrZfmoLnpgoTljp8NCmBgYHtyfQ0KY29ycCA9IHRtX21hcChjb3JwLCByZW1vdmVXb3Jkcywgc3RvcHdvcmRzKCJlbmdsaXNoIikpDQpjb3JwID0gdG1fbWFwKGNvcnAsIHN0ZW1Eb2N1bWVudCkNCmR0bTIgPSBEb2N1bWVudFRlcm1NYXRyaXgoY29ycCk7IGR0bTIgICAgICAgICMgdGVybXM6IDE3NzkxMQ0KZHRtMiA9IHJlbW92ZVNwYXJzZVRlcm1zKGR0bTIsIC45OTk5KTsgZHRtMiAgIyB0ZXJtczogMTI5NjgNCmBgYA0KPGJyPg0KDQpgYGB7cn0NCnNhdmUoZHRtMSxkdG0yLGZpbGU9J2RhdGEvZHRtLnJkYXRhJykNCmBgYA0K5YWI5a2Y6LW35L6G77yM5YWN5b6X5q+P5qyh6YO96KaB6YeN5YGa44CCIDxicj4NCjxicj4NCg0KLSAtIC0NCjxicj4NCg0KIyMgKDIpIFByZXBhcmF0aW9uDQoNCmBgYHtyfQ0KbG9hZCgnZGF0YS9kdG0ucmRhdGEnKQ0KRFRNID0gZHRtMSAlPiUgey5bLCBvcmRlcigtY29sX3N1bXMoLikpXX0NCmBgYA0K54K65LqG6KeA5a+f5pa55L6/77yM5oiR5YCR6YG45pOH5a6M5pW05a2X6KmeKOacqumChOWOn+Wtl+aguSnnmoRgRFRNYO+8jOiugOmAsmBEVE1g5LmL5b6M77yM6YCa5bi45YWI5oqK5a2X6Kme5qyE5L2N5L6d5a2X6Kme55qE5Ye654++6KmV546H5o6S5YiX44CCPGJyPg0KDQpgYGB7cn0NCkVmZmVjdCA9IGZ1bmN0aW9uKHksIHgsIG09cmVwKFRSVUUsIGxlbmd0aCh5KSkpIHsNCiAgeCA9IHhbbV07IHkgPSB5W21dDQogIG4gPSBhcy5udW1lcmljKGxlbmd0aCh4KSkNCiAgcFggPSBzdW0oeCkvbjsgcFkgPSBzdW0oeSkvbjsgcFhZID0gc3VtKHgmeSkvbg0KICBlZiA9IGModXNhZ2U9cFgsIGJhc2U9cFksIHN1cHBvcnQ9cFhZLCBjb25mPXBYWS9wWCwgbGlmdD1wWFkvcFgvcFkpDQogIGMocm91bmQoMTAwKmVmLCAyKSwgY291bnQ9bikgfQ0KYGBgDQrlrprnvqnkuIDlgItmdW5jdGlvbihgRWZmZWN0KClgKeS+huioiOeul2BYYOWwjWBZYOeahOWQhOeoruaViOaenO+8jOWMheaLrO+8mg0KDQorIFVzYWdlIChgUHJbWF1gKSAtLSB0aGUgdXNhZ2UgcmF0aW8gb2YgYFhgIA0KKyBCYXNlIChgUHJbWV1gKSAtLSB0aGUgb3ZlcmFsbCBwcm9iYWJpbGl0eSBvZiBgWWANCisgQ29uZmlkZW5jZSAoYFByW1l8WF1gKSAtLSB0aGUgcHJvYmFiaWxpdHkgb2YgYFlgIGdpdmVuIGBYYA0KKyBTdXBwb3J0IChgUHJbWV5YXWApIC0tIHRoZSBwcm9iYWJpbGl0eSBvZiBgWF5ZYA0KKyBMaWZ0IChgUHJbWXxYXS9QcltZXWApIC0tIGBYYCdzIGVmZmVjdCBvbiBgWWAgKHRoZSBsaWZ0IG9mIGBZYCdzIHByb2JhYmlsaXR5IGdpdmVuIGBYYCkNCisgQ291bnQgLS0gdGhlIGxlbmd0aCBvZiB0aGUgdmVjdG9ycyAoYFhgIGFuZCBgWWAgc2hvdWxkIGhhdmUgdGhlIHNhbWUgbGVuZ3RoKQ0KDQoNCmBgYHtyfQ0KWSA9IHJldmlld1ssInVzZWZ1bCJdICU+JSB7LiA+IG1lZGlhbiguKX0NCkVmZmVjdChZLCBhcy52ZWN0b3IoRFRNWywicGl6emEiXSkgPiAwKQ0KYGBgDQpgcGl6emFgIGhhcyBubyBlZmZlY3Qgb24gYHJldmlldyR1c2VmdWxgIChpdHMgYGxpZnRgIGlzIGJhcmVseSBoaWdoZXIgdGhhbiAxMDAlLikNCg0KDQpgYGB7cn0NCkVmZmVjdChZLCBhcy52ZWN0b3IoRFRNWywiZHJlc3NlcyJdKSA+IDApDQpgYGANCmBkcmVzc2VzYCBoYXMgYSBwb3NpdGl2ZSBlZmZlY3Qgb24gYHJldmlldyR1c2VmdWxgIChpdHMgYGxpZnRgIGlzIGhpZ2hlciB0aGFuIDE1MCUuKTxicj4gPGJyPiANCg0KIyMjIDIuMSAtLSBUaGUgZWZmZWN0IG9mIDIwIG1vc3QgZnJlcXVlbnQgd29yZHMNCk5vdyB3ZSBjYW4gb2JzZXJ2ZSB0aGUgZWZmZWN0IG9mIHRoZSBtb3N0IGZyZXF1ZW50ICgyMCkgd29yZHMgLi4uIA0KYGBge3J9DQpkZiA9IHQoc2FwcGx5KGNvbG5hbWVzKERUTSlbMToyMF0sIGZ1bmN0aW9uKHcpIA0KICBFZmZlY3QoWSwgYXMudmVjdG9yKERUTVssIHddKSA+IDApKSkNCmRmDQpgYGANCkFzIHlvdSBoYXZlIGp1c3QgZXhwZXJpZW5jZWQsIGNhbGN1bGF0aW5nIHRoZSBlZmZlY3QgaXMgYSBsZW5ndGh5IHRhc2suIExldCdzIHR1cm4gb24gcGFyYWxsZWwgY29tcHV0YXRpb24uDQoNCmBgYHtyfQ0KbGlicmFyeShkb1BhcmFsbGVsKQ0KSyA9IDQ7IGNsdXN0PW1ha2VDbHVzdGVyKEspDQpyZWdpc3RlckRvUGFyYWxsZWwoY2x1c3QpDQpnZXREb1BhcldvcmtlcnMoKQ0KYGBgDQo8YnI+DQoNCi0gLSAtDQo8YnI+DQoNCiMjICgzKSBXb3JkcycgRWZmZWN0IG9uIHRoZSBFbnRpcmUgQ29ycHVzDQoNCiMjIyAzLjEgLS0gVGhlIE1vc3QgRnJlcXVlbnQgV29yZHMNClBpY2sgdGhlIG1vc3QgZnJlcXVlbnQgd29ya3MgKDM2MCkgZnJvbSB0aGUgYERUTWAuIFNpbmNlIHdlIGhhZCBzb3J0ZWQgYERUTWAgYnkgZGVzY2VuZGluZyBgY29sX3N1bXMoKWAsIHdlIHNpbXBseSB0YWtlIHRoZSBmaXJzdCAzNjAgYGNvbG5hbWVzKClgIGZyb20gaXQuIEl0J2QgdGFrZSBzb21lIHRpbWUgZXZlbiB3aXRoIHBhcmFsbGVsIHByb2Nlc3NpbmcgKH42MCBzZWNvbmRzIGluIG15IG5vdGVib29rKS4gU28sIGJlIHBhdGllbnQuICAgDQpgYGB7cn0NCldvcmRzID0gY29sbmFtZXMoRFRNKVsxOjM2MF0gDQp0MCA9IFN5cy50aW1lKCkNCmRmID0gZm9yZWFjaCh3b3JkID0gV29yZHMsIC5jb21iaW5lPXJiaW5kKSAlZG9wYXIlIHsNCiAgbGlicmFyeShzbGFtKQ0KICBFZmZlY3QoWSwgcm93X3N1bXMoRFRNWyx3b3JkXSkgPiAwKSB9DQpTeXMudGltZSgpIC0gdDANCmBgYA0KDQpgYGB7cn0NCmRmID0gZGF0YS5mcmFtZShkZiwgd29yZD1Xb3JkcykNCmRmWzE6NixdDQpgYGANCg0KYGBge3J9DQpoY2hhcnQoZGYsInNjYXR0ZXIiLGhjYWVzKA0KICB5PXVzYWdlLHg9bGlmdCxjb2xvcj1jb25mLHNpemU9Y29uZixncm91cD13b3JkKSkgJT4lIA0KICBoY19sZWdlbmQoZW5hYmxlZD1GKSAlPiUgaGNfY2hhcnQoem9vbVR5cGU9Inh5IikgJT4lIA0KICBoY19hZGRfdGhlbWUoaGNfdGhlbWVfZmxhdCgpKSAlPiUgDQogIGhjX3Bsb3RPcHRpb25zKGJ1YmJsZT1saXN0KG1heFNpemU9IjIlIixtaW5TaXplPTEwKSkgJT4lIA0KICBoY194QXhpcyhwbG90TGluZXM9bGlzdChsaXN0KGNvbG9yPSJwaW5rIix2YWx1ZT1tZWFuKGRmJGxpZnQpLHdpZHRoPTIpKSkgJT4lIA0KICBoY190aXRsZSh0ZXh0PSJFZmZlY3Qgb2YgdGhlIE1vc3QgRnJlcXVlbnQgV29yZHMgKDM2MCkgb24gVXNlZnVsbmVzcyIpDQpgYGANCjxicj4NCkluIHRoZSBmaWd1cmUgYWJvdmUsIGVhY2ggYnViYmxlIHJlcHJlc2VudHMgYSB3b3JkLiBUaGUgYnViYmxlcycgLi4uDQoNCiogQ29sb3JzIHJlcHJlc2VudCB0aGUgYGNvbmZpZGVuY2U6IFByW1l8WF1gOyB5ZWxsb3cvYmx1ZSBpbmRpY2F0ZXMgaGlnaC9sb3cgY29uZmlkZW5jZS4NCiAgICArIFRoZSBjb25maWRlbmNlcyBhcmUsIGJ5IGRlZmluaXRpb24sIGNsb3NlbHkgY29ycmVsYXRlZCB0byB0aGUgbGlmdC48YnI+DQogICAgDQoqIFgtY29vcmRpbmF0ZSByZXByZXNlbnQgdGhlIGBsaWZ0OiBQcltZfFhdL1ByW1ldYCBpbiBwZXJjZW50YWdlLg0KICAgICsgT24gdGhlIHJpZ2h0LCB3ZSBjYW4gc2VlIHRoZSB3b3JkcyBvZiB0aGUgaGlnaGVzdCBsaWZ0LCBpbmNsdWRpbmfvvJogYHllc2AsIGByZXZpZXdgLCBgdGhvc2VgLCBgaW50b2AsIGBmaW5hbGx5YDsNCiAgICArIE9uZSB0aGUgbGVmdCwgd2UgY2FuIHNlZSBzb21lIHdvcmRzIG9mIGxvdy9uZWdhdGl2ZSBsaWZ0Lg0KICAgICsgSW50ZXJlc3RpbmdseSwgc3VjaCB3b3JkcyBvZiBwb3NpdGl2ZSBpbXBsaWNhdGlvbnMgYXMgYGdyZWF0YCwgYGV4Y2VsbGVudGAsIGByZWNvbW1lbmRgIGFsbCBjYXJyeSBuZWdhdGl2ZSBsaWZ0cy4NCiAgICArIE9uIHRoZSB1cHBlciBsZWZ0LCB3ZSBoYXZlIHRoZSB0d28gbW9zdCBmcmVxdWVudCB3b3JkcyBpbiBFbmdsaXNoIC0tIGB0aGVgIGFuZCBgYW5kYC4gDQogICAgKyBUaGUgcG9zaXRpdmUgYmlhcyBvZiBsaWZ0IGlzIHF1aXRlIG9idmlvdXMuIEV2ZW4gc3VjaCBjb21tb24gd29yZHMgYXMgYHRoZWAgYW5kIGBhbmRgIGNhcnJ5IHBvc2l0aXZlIGxpZnRzLiBUbyBjb3BlIHdpdGggdGhpcyBiaWFzLCB3ZSB3aWxsIGFkanVzdCB0aGUgbmV1dHJhbCBsaWZ0IGZyb20gMTAwIHRvIDEyNSAodGhlIG1lYW4gb2YgdGhlIGxpZnQpLjxicj4NCiAgICANCiogWS1jb29yZGluYXRlIHJlcHJlc2VudCB0aGUgYHVzYWdlOiBQcltYXWAuDQogICAgKyBMaWZ0IGlzIG5lZ2F0aXZlbHkgY29ycmVsYXRlZCB3aXRoIHVzYWdlOyBhbGwgb2YgdGhlIGhpZ2gtbGlmdCB3b3JkcyBleGhpYml0cyBsb3ctdXNhZ2UuPGJyPg0KPGJyPg0KDQojIyMgMy4yIC0tIFNlbGVjdCBXb3JkcyB3aXRoIFRGLUlERg0KVXN1YWxseSwgdGhlIG1vc3QgaW1wb3J0YW5jZSB3b3JkcyBhcmUgbm90IHRoZSBzYW1lIGFzIHRoZSBtb3N0IGZyZXF1ZW50IHdvcmRzLiBXZSBjYW4gdXNlIHRoZSBbVEYtSURGXShodHRwOi8vemgud2lraXBlZGlhLm9yZy93aWtpL1RmLWlkZikgKFRlcm0gRnJlcXVlbmN5IC0tIEludmVyc2UgRG9jdW1lbnQgRnJlcXVlbmN5KSBtZXRob2QgdG8gcGljayB0aGUgaW1wb3J0YW50IHlldCBsZXNzIGZyZXF1ZW50IHdvcmRzLiANCmBgYHtyfQ0KdGZpZGYgPSBEVE0gJT4lIHt0YXBwbHkoLiR2L3Jvd19zdW1zKC4pWy4kaV0sIC4kaiwgbWVhbikgKg0KICAgIGxvZzIoIG5Eb2NzKC4pIC8gY29sX3N1bXMoLiA+IDApICl9DQpURnhJREYgPSBEVE1bLHdoaWNoKHRmaWRmWzE6MzAwMF0gPiBxdWFudGlsZSh0ZmlkZilbM10pXSAlPiUNCiAgY29sX3N1bXMoKSAlPiUgc29ydCAlPiUgbmFtZXMNCmxlbmd0aChURnhJREYpDQpgYGANCjxicj4NClRoZSBhbGdvcml0aG0gZ2VuZXJhdGUgMzc3IHdvcmRzLiBUbyBiZSBjb25zaXN0ZW50LCB3ZSBvbmx5IHRha2UgdGhlIGZpcnN0IDM2MCB3b3Jkcy4gIFdlIHB1dCB0aGUgd29yZHMgaW4gYFdvcmRzYCBhbmQgdXNlIHRoZSBzYW1lIGNvZGUgdG8gY2FsY3VsYXRlIHRoZSBlZmZlY3RzIG9mIHRoZXNlIHdvcmRzIC4uLg0KYGBge3J9DQpXb3JkcyA9IFRGeElERlsxOjM2MF0NCnQwID0gU3lzLnRpbWUoKQ0KZGYgPSBmb3JlYWNoKHdvcmQgPSBXb3JkcywgLmNvbWJpbmU9cmJpbmQpICVkb3BhciUgew0KICBsaWJyYXJ5KHNsYW0pDQogIEVmZmVjdChZLCByb3dfc3VtcyhEVE1bLHdvcmRdKSA+IDApIH0NClN5cy50aW1lKCkgLSB0MCANCmBgYA0KYW5kIHRoZW4gbWFrZSBhIGludGVyYWN0aXZlIGNoYXJ0Lg0KDQpgYGB7cn0NCmRmID0gZGF0YS5mcmFtZShkZiwgd29yZD1Xb3JkcykNCmhjaGFydChkZiwic2NhdHRlciIsaGNhZXMoDQogIHk9dXNhZ2UseD1saWZ0LGNvbG9yPWNvbmYsc2l6ZT1jb25mLGdyb3VwPXdvcmQpKSAlPiUgDQogIGhjX2xlZ2VuZChlbmFibGVkPUYpICU+JSBoY19jaGFydCh6b29tVHlwZT0ieHkiKSAlPiUgDQogIGhjX2FkZF90aGVtZShoY190aGVtZV9mbGF0KCkpICU+JSANCiAgaGNfcGxvdE9wdGlvbnMoYnViYmxlPWxpc3QobWF4U2l6ZT0iMiUiLG1pblNpemU9MTApKSAlPiUNCiAgaGNfeEF4aXMocGxvdExpbmVzPWxpc3QobGlzdChjb2xvcj0icGluayIsdmFsdWU9bWVhbihkZiRsaWZ0KSx3aWR0aD0yKSkpICU+JSANCiAgaGNfdGl0bGUodGV4dD0iRWZmZWN0IG9mIFRGeElERiBXb3JkcyAoMzYwLSwgY2Vuc29yZWQpIG9uIFVzZWZ1bG5lc3MiKQ0KYGBgDQo8YnI+DQoNClRoZXJlIGFyZSBzb21lIG1ham9yIGRpZmZlcmVuY2UgYmV0d2VlbiB0aGUgdHdvIGNoYXJ0czoNCg0KMS4gVGhlIF9fU2NhbGVfXyA6IFRoZSB1c2FnZXMgb2YgVEYtSURGIHdvcmRzIGFyZSBsb3dlciwgYnV0IHRoZWlyIGxpZnRzIGFyZSBtb3JlIGRpdmVyc2UgdGhhbiB0aGUgZnJlcXVlbnQgd29yZHMuDQogICsgIEJlY2F1c2UgW1RGLUlERl0oaHR0cDovL3poLndpa2lwZWRpYS5vcmcvd2lraS9UZi1pZGYpIHJhdGVzIHRoZSB3b3JkcyBieSBfX0ludmVyc2VfXyBkb2N1bWVudCBmcmVxdWVuY3ksIHN1Y2ggY29tbW9uIHdvcmRzIGFzIGB0aGlzYCBhbmQgYHRoYXRgIGFyZSBwcmVjbHVkZWQuIEluIHRoaXMgd2F5LCBpdCBoZWxwcyB0byBmaW5kIHRoZSBsZXNzIGZyZXF1ZW50IGJ1dCBpbXBvcnRhbnQgd29yZHMuDQogICsgVG8gYXZpb2QgZ2xpdGNoZXMsIHdlIGNlbnNvciB0aGUgd29yZHMgd2l0aCB1c2FnZSBsb3dlciB0aGFuIDAuMSUuIFRoZSB1cHBlciBib3VuZCBvZiB1c2FnZSBhcmUgbWVyZWx5IDEuMzMlLCB3aGlsc3QgdGhhdCBvZiBmcmVxdWVudCB3b3JkcyBpcyA5MS40OCUuDQogICsgQWx0aG91Z2ggdGhlIHVzYWdlIG9mIHRoZXNlIHdvcmRzIGFyZSBsb3csIHRoZSBzcHJlYWQgb2YgdGhlaXIgbGlmdHMgYXJlIG11Y2ggd2lkZXIgLS0gWzc1LjklLCAyMDUuOCVdIGNvbXBhcmluZyB0byBbOTguNiUsIDE1My40JV0uPGJyPg0KICANCjIuIFRoZSBfX0Rpc3RyaWJ1dGlvbiBvZiBMaWZ0X18gOiBUaGUgbGlmdHMgZXhoaWJpdCBhIG5vcm1hbC1hbGlrZS1kaXN0cmlidXRpb24uIFdpdGggdGhlIG1lYW4gbGlmdHMgaXMgYXJvdW5kIDEzMCUuIFRoZXJmb3JlIGl0IGlzIGVhc2llciB0byBpZGVudGlmeSB0aGUgJ2dvb2QnIGFuZCAnYmFkJyB3b3Jkcy4gPGJyPg0KDQozLiBUaGUgX19Hb29kc19fIGFyZTogYGZlemAsIGBnYXlgLCBgcmVwdWJsaWNgLCBgbG9sb3NgLCBgaG9vZGAsIC4uLiA8YnI+DQoNCjQuIFRoZSBfX0JhZHNfXyBhcmU6IGBkZWZpbmF0ZWx5YCwgYGhhaXJjdXRgLCBgYXV0b2AsIGByZXBhaXJgLCBgZ2x1dGVuYCwgLi4uIDxicj4NCjxicj4NCg0KLSAtIC0NCg0KPGJyPg0KDQojIyAoNCkgV29yZHMnIEVmZmVjdCBBY3Jvc3MgVHdvIFN1Yi1Db3JwdXMNCg0K6Zmk5LqG6Zec5b+D5a2X6Kme5Zyo5pW05YCL5paH6ZuG55qE6KGo54++77yM5oiR5YCR5Lmf5Y+v5Lul5q+U6LyD5a2X6Kme5Zyo5LiN5ZCM56iu6aGe55qE5a2Q5paH6ZuG5LmL5Lit55qE5pWI5p6c44CCDQoNCiMjIyA0LjEgLS0gSGVscGVyIEZ1bmN0aW9uDQoNCuS4gOWAi+Wtl+WcqOS4jeWQjOWtkOaWh+mbhuacg+acieS4jeWQjOeahGxpZnTvvIzlpoLmnpzmiJHlgJHnlKjpgJnkuptsaWZ05YGa5bqn5qiZ77yM5oqK5LiA5pW0576k5a2X55Wr5Zyo5ZCM5LiA5YCL5bmz6Z2i5LiK77yM5oiR5YCR5bCx5Y+v5Lul5q+U6LyD5a2X5Zyo5a2Q5paH6ZuG55qE5pWI5p6c44CCIOaIkeWAkeWFiOWBmuS4gOWAi+ijveWclueahGhlbHBlciBmdW5jdGlvbu+8jOWPq+Wug2BXb3Jkc0xpZnQoKWAuIA0KDQpgYGB7cn0NCldvcmRzTGlmdCA9IGZ1bmN0aW9uKEwsIE9uPSJVc2VmdWxuZXNzIiwgYzE9IkNhdDEiLCBjMj0iQ2F0MiIsIFdkPSJGcmVxdWVudCIpIHsNCiAgdHRsID0gc3ByaW50ZigiRWZmZWN0IG9mICVzIFdvcmRzIG9uICVzIiwgV2QsIE9uKQ0KICBzdWIgPSBzcHJpbnRmKCJTaXplOiBUb3RhbCBVc2FnZTsgQ29sb3I6ICVzKHllbGxvdykgfCAlcyhibHVlKSIsIGMyLCBjMSkNCiAgdHRseCA9IHNwcmludGYoIiUlIExpZnQgb24gJXMgKGJsdWUpIiwgYzEpDQogIHR0bHkgPSBzcHJpbnRmKCIlJSBMaWZ0IG9uICVzICh5ZWxsb3cpIiwgYzIpDQogIA0KICBkZiA9IG1lcmdlKExbWzFdXSxMW1syXV0sYnk9J3dvcmQnLHNvcnQ9RixzdWZmaXhlcz1jKCJfeCIsIl95IikpDQogIGRmID0gZGZbZGYkdXNhZ2VfeCA+IDAuMSAmIGRmJHVzYWdlX3kgPiAwLjEsIF0gDQogIGRmJHJhdGlvID0gcm91bmQoZGYkdXNhZ2VfeSAvIGRmJHVzYWdlX3gsIDMpDQogIGRmJHRvdGFsID0gcm91bmQoZGYkdXNhZ2VfeSArIGRmJHVzYWdlX3gsIDMpDQogIA0KICB0aXBzPXBhc3RlMCgiPGI+e3BvaW50LndvcmR9PC9iPjxicj4iLA0KICAgICAgICAgICAgICAiY29uZjogKHtwb2ludC5jb25mX3h9JSwge3BvaW50LmNvbmZfeX0lKTxicj4iLA0KICAgICAgICAgICAgICAidXNhZ2U6ICh7cG9pbnQudXNhZ2VfeH0lLCB7cG9pbnQudXNhZ2VfeX0lKTxicj4iLA0KICAgICAgICAgICAgICAidG90YWwgdWFzZ2UgKHkgKyB4KToge3BvaW50LnRvdGFsfSU8YnI+IiwNCiAgICAgICAgICAgICAgInVhc2dlIHJhdGlvICh5IC8geCk6IHtwb2ludC5yYXRpb30iKQ0KICANCiAgaGNoYXJ0KGRmLCJzY2F0dGVyIixoY2FlcygNCiAgICB4PWxpZnRfeCwgeT1saWZ0X3ksIHNpemU9bG9nKHRvdGFsKSwgY29sb3I9bG9nKHJhdGlvKSkpICU+JSANCiAgICBoY19jaGFydCh6b29tVHlwZT0ieHkiKSAlPiUgaGNfYWRkX3RoZW1lKGhjX3RoZW1lXzUzOCgpKSAlPiUgDQogICAgaGNfcGxvdE9wdGlvbnMoYnViYmxlPWxpc3QobWF4U2l6ZT0iMiUiLG1pblNpemU9NCkpICU+JSANCiAgICBoY194QXhpcyh0aXRsZT1saXN0KHRleHQ9dHRseCkpICU+JSBoY195QXhpcyh0aXRsZT1saXN0KHRleHQ9dHRseSkpICU+JSANCiAgICBoY190b29sdGlwKGhlYWRlckZvcm1hdD0iIixoaWRlRGVsYXk9MTAwLHVzZUhUTUw9VCxwb2ludEZvcm1hdD10aXBzKSAlPiUNCiAgICBoY194QXhpcyhwbG90TGluZXM9bGlzdChsaXN0KGNvbG9yPSJvcmFuZ2UiLHZhbHVlPW1lYW4oZGYkbGlmdF94KSx3aWR0aD0yKSkpICU+JSANCiAgICBoY195QXhpcyhwbG90TGluZXM9bGlzdChsaXN0KGNvbG9yPSJvcmFuZ2UiLHZhbHVlPW1lYW4oZGYkbGlmdF95KSx3aWR0aD0yKSkpICU+JSANCiAgICBoY190aXRsZSh0ZXh0PXR0bCkgJT4lIGhjX3N1YnRpdGxlKHRleHQ9c3ViKQ0KfQ0KYGBgDQoNCg0KIyMjIDQuMiAtLSBGb29kIHZzLiBOb24tRm9vZCwgMzYwIEZyZXF1ZW50IFdvcmRzDQoNCuWcqGBDYXRHcm91cGDoo6HpnaLlrprnvqnlhanlgIvlrZDmlofpm4bvvJoNCg0KKyBgRm9vZGA6IHRoZSBgUmVzdGF1cmFudHNgIGFuZCBgRm9vZGAgY2F0ZWdvcmllcw0KKyBgbm9uRm9vZGA6IG90aGVycw0KDQrmioropoHliIbmnpDnmoTlrZfmlL7lnKhgV29yZHNg6KOh6Z2i77yM54S25b6M55So5LiA5YCL6L+05ZyI77yM5bCN5YWp5YCL5a2Q5paH6ZuG5YiG5Yil5YGa5pWI5p6c5YiG5p6Q77yM5bCH57WQ5p6c5pS+5ZyoYExg6YCZ5YCLbGlzdOijoemdouOAgg0KDQpgYGB7cn0NCmJpZHMgPSByb3dTdW1zKG14QkNbLGMoJ1Jlc3RhdXJhbnRzJywnRm9vZCcpXSkgPiAwDQpDYXRHcm91cCA9IGxpc3QoRm9vZD1iaWRzLCBuRm9vZD0hYmlkcykNCldvcmRzID0gY29sbmFtZXMoRFRNKVsxOjM2MF0NCg0KTCA9IGxpc3QoKTsgZm9yKGkgaW4gMTpsZW5ndGgoQ2F0R3JvdXApKSB7DQogIHJtYXNrID0gcmV2aWV3JGJpZCAlaW4lIGJpeiRiaWRbIENhdEdyb3VwW1tpXV0gXQ0KICBkZiA9IGZvcmVhY2god29yZCA9IFdvcmRzLCAuY29tYmluZT1yYmluZCkgJWRvcGFyJSB7DQogICAgRWZmZWN0KFksIHJvd19zdW1zKERUTVssd29yZF0pID4gMCwgcm1hc2spIH0NCiAgZGYgPSBkYXRhLmZyYW1lKGRmLCB3b3JkPVdvcmRzLCBjYXRncnA9bmFtZXMoQ2F0R3JvdXApW2ldKQ0KICBMW1tpXV0gPSBkZn0NCmZvb2QuZnJlcSA9IEwNCmBgYA0KDQrkuYvlvozlsLHlj6/ku6XnlKhgV29yZHNMaWZ0KClg6KO95ZyW44CCDQpgYGB7cn0NCldvcmRzTGlmdChmb29kLmZyZXEsICJVc2VmdWxuZXNzIiwgIkZvb2QiLCAibm9uRm9vZCIsICJGcmVxdWVudCAoMzYwKSIpDQpgYGANCjxicj4NCg0K5ZyW5Lit5q+P5LiA6bue5Luj6KGo5LiA5YCL5a2XIC4uLg0KDQorIOm7nueahOapqyjntpwp5bqn5qiZ5Luj6KGo5a2X5bCNYEZvb2RgKGBub25Gb29kYCnlrZDpm4bnmoRsaWZ0cw0KKyDpu57nmoTlpKflsI/ku6PooajlrZfnmoTkvb/nlKjnjocgKHRoZSBzdW0gb2YgdXNhZ2UsIGxvZyB0cmFuc2ZlcmVkKQ0KKyDpu57nmoTpoY/oibLku6PooajlrZflh7rnj77lnKhgRm9vZGAo6JeN6ImyKeWSjGBub25Gb29kKOm7g+iJsinlrZDpm4bnmoTmr5Tph40NCg0KPGJyPg0KV2UgY2FuIG9ic2VydmUgdGhhdDoNCg0KKyBBcyB3ZSdkIG9ic2VydmVkIGluIHRoZSBwcmV2aW91cyBjaGFydHMsIGxpZnQgaXMgbmVnYXRpdmVseSBjb3JyZWxhdGVkIHdpdGggdXNhZ2UuDQorIExpZnQgb2YgYEZvb2RgIGlzIHBvc2l0aXZlbHkgY29ycmVsYXRlZCB3aXRoIGBub25Gb29kYA0KKyBJbiB0aGUgdXBwZXIgbGVmdCwgc29tZSBmb29kIHJlbGF0ZWQgd29yZHMgKGBwaXp6YWAsIGB0YWNvc2AsIGBzdXNoaWAsIGBjaGlwc2AsIGFuZCBgc3BpY3lgKSBhcmUgaGF2ZSBsb3cvaGlnaC1saWZ0IG9uIGBGb29kYC9gbm9uRm9vZGAuPGJyPg0KDQo8YnI+DQoNCiMjIyA0LjMgLS0gRm9vZCB2cy4gTm9uLUZvb2QsIDE5NCBURi1JREYgV29yZHMNCg0KTGV0J3MgcmVwZWF0IHRoZSBwcm9jZXNzIGluIHRoZSBwcmV2aW91cyBzdWItc2Vzc2lvbiB3aXRoIFRGLUlERiB3b3Jkcy4gIA0KDQpgYGB7cn0NCldvcmRzID0gVEZ4SURGWzE6MzYwXQ0KZm9vZC50ZmlkZiA9IEwgPSBsaXN0KCk7IGZvcihpIGluIDE6bGVuZ3RoKENhdEdyb3VwKSkgew0KICBybWFzayA9IHJldmlldyRiaWQgJWluJSBiaXokYmlkWyBDYXRHcm91cFtbaV1dIF0NCiAgZGYgPSBmb3JlYWNoKHdvcmQgPSBXb3JkcywgLmNvbWJpbmU9cmJpbmQpICVkb3BhciUgew0KICAgIEVmZmVjdChZLCByb3dfc3VtcyhEVE1bLHdvcmRdKSA+IDAsIHJtYXNrKSB9DQogIGRmID0gZGF0YS5mcmFtZShkZiwgd29yZD1Xb3JkcywgY2F0Z3JwPW5hbWVzKENhdEdyb3VwKVtpXSkNCiAgTFtbaV1dID0gZGZ9DQpmb29kLnRmaWRmID0gTA0KYGBgDQoNCmBgYHtyfQ0KV29yZHNMaWZ0KGZvb2QudGZpZGYsICJVc2VmdWxuZXNzIiwgIkZvb2QiLCAibm9uRm9vZCIsIA0KICAgICAgICAgIlRGeElERiAoMzYwLCBjZW5zb3JlZCBieSAwLjElIHVzYWdlKSIpDQpgYGANCjxicj4NCg0KVG8gaW1wcm92ZSB0aGUgY29tcGFyYXRpdmUgdmFsaWRpdHksIHdlIGNlbnNvciB0aGUgd29yZHMgd2l0aCB1c2FnZSBsb3dlciB0aGFuIDAuMSUgaW4gZWl0aGVyIHN1Yi1jb3JwdXMuIFRoZXJlYnksIHRoZSB3b3JkcyBkaXNwbGF5ZWQgYXJlIDE3NCwgaW5zdGVhZCBvZiAzNjAuICBBcyB3ZSBjYW4gc2VlIC4uLg0KDQoxLiB0aGUgZGlzdHJpYnV0aW9uIG9mIGxpZnQgb2YgVEYtSURGIGlzIHdpZGVyIHRoYW4gdGhhdCBvZiBmcmVxdWVudCB3b3Jkcy4gIA0KMi4gVGhlIGNvcnJlbGF0aW9uIGJldHdlZW4gbGlmdCBhbmQgdXNhZ2UgcmVtYWlucyANCjMuIEJ1dCwgdGhlIGNvcnJlbGF0aW9uIGJldHdlZW4gYEZvb2RgIGFuZCBgbm9uRm9vZGAgaXMgbm8gbG9uZ2VyIHNpZ25pZmljYW50Lg0KNC4gQnkgbWVkaWFuIHNwbGl0LCB3ZSBjYW4gZGl2aWRlIHRoZSBwbGFuZSBpbnRvIGZvdXIgcXVhcnRlcnMuIE9uIHRoZSAuLi4NCiAgICArIHVwcGVyIHJpZ2h0IC0tIGBnYXlgIGFuZCBgcmVwdWJsaWNgIGlzIGdvb2QgZm9yIGJvdGggc3ViLWNvcnBzZXMNCiAgICArIGxvd2VyIGxlZnQgLS0gYGRlZmluYXRlbHlgIGlzIGJhZCBmb3IgYm90aCBzdWItY29ycHVzDQogICAgKyBsb3dlciByaWdodCAtLSBgbWlrZWAsIGBwb2xpc2hgLCBgY2hyaXNgIGFuZCBgcmVzdWx0YCBhcmUgZ29vZCBpbiBgRm9vZGAgYnV0IGJhZCBpbiBgbm9uRm9vZGANCiAgICArIGxvd2VyIGxlZnQgLS0gYG1hZ2FyaXRhc2AgYW5kIGBwZWFrc2AgYXJlIGdvb2QgaW4gYG5vbkZvb2RgIGJ1dCBiYWQgaW4gYEZvb2RgDQoNCjxicj4NCjxicj4NCg0KLSAtIC0NCg0KPGJyPg0KDQojIyMgNC40IC0tIEJhcnMgdnMgU2hvcHBpbmcsIDEwMDAgRnJlcXVlbnQgV29yZHMgDQoNCkFzIGFuIGV4ZXJjaXNlLCB3ZSBjb21wYXJlIHRoZSBlZmZlY3Qgb2YgDQoNCisgdGhlIG1vc3QgX19mcmVxdWVudF9fIDEwMDAgd29yZHMgDQorIG9uIGByZXZpZXckY29vbGANCisgYWNyb3NzIGBCYXJzYCBhbmQgYFNob3BwaW5nYCBjYXRlZ29yaWVzDQoNClNpbXBseSBwdXQgdGhlIGNyaXRlcmlhIGluIGBZYCwgYENhdEdyb3VwYCBhbmQgYFdvcmRzYCAuLi4NCmBgYHtyfQ0KWSA9IHJldmlld1ssImNvb2wiXSAlPiUgey4gPiBtZWRpYW4oLil9DQpDYXRHcm91cCA9IGxpc3QoQmFyID0gbXhCQ1ssJ0JhcnMnXSwgU2hvcHBpbmcgPSBteEJDWywnU2hvcHBpbmcnXSkNCldvcmRzID0gY29sbmFtZXMoRFRNKVsxOjEwMDBdDQpgYGANCg0KSXQgdGFrZSBhYm91dCA1IG1pbnV0ZXMgdG8gZXZhbHVhdGUgdGhlIGVmZmVjdCBvZiAxMDAwIHdvcmRzLiANCmBgYHtyfQ0KdDAgPSBTeXMudGltZSgpDQpiYXJzX3Nob3AgPSBMID0gbGlzdCgpOyBmb3IoaSBpbiAxOmxlbmd0aChDYXRHcm91cCkpIHsNCiAgcm1hc2sgPSByZXZpZXckYmlkICVpbiUgYml6JGJpZFsgQ2F0R3JvdXBbW2ldXSBdDQogIGRmID0gZm9yZWFjaCh3b3JkID0gV29yZHMsIC5jb21iaW5lPXJiaW5kKSAlZG9wYXIlIHsNCiAgICBFZmZlY3QoWSwgcm93X3N1bXMoRFRNWyx3b3JkXSkgPiAwLCBybWFzaykgfQ0KICBkZiA9IGRhdGEuZnJhbWUoZGYsIHdvcmQ9V29yZHMsIGNhdGdycD1uYW1lcyhDYXRHcm91cClbaV0pDQogIExbW2ldXSA9IGRmfQ0KU3lzLnRpbWUoKSAtIHQwIA0KYmFyc19zaG9wID0gTA0KYGBgDQoNCldlIGNhbiBwbG90IHRoZSBkYXRhIHdpdGggdGhlIHNhbWUgaGVscGVyIGZ1bmN0aW9uIChgV29yZHNMaWZ0KClgKSANCmBgYHtyfQ0KV29yZHNMaWZ0KGJhcnNfc2hvcCwgIkNvb2xuZXNzIiwgIkJhcnMiLCAiU2hvcHBpbmciLCAiRnJlcXVlbnQgKDEwMDApIikNCmBgYA0KPGJyPg0KDQpfX1FVSVogOl9fDQoNCjEuIFdoYXQgYXJlIHlvdXIgb2JzZXJ2YXRpb25zIGluIHRoZSBjaGFydCBhYm92ZT8NCjIuIFRyeSB0byANCiAgICArIHBpY2sgdHdvIGdyb3VwcyBjYXRlZ29yaWVzDQogICAgKyBtYWtlIGEgbGlmdCBjb21wYXJpc29uIGNoYXJ0IHdpdGggdGhlIDM2MCBtb3N0IGZyZXF1ZW50IHdvcmRzDQogICAgKyBtYWtlIGEgbGlmdCBjaGFydCB3aXRoIHRoZSBURi1JREYgd29yZHMNCiAgICArIHNoYXJlIHdpdGggdXMgeW91ciBtYWpvciBmaW5kaW5ncyAuLi4NCg0KPGJyPg0KYGBge3J9DQojIHNhdmUgYmVmb3JlIGxlYXZpbmcgDQpzYXZlKGJhcnNfc2hvcCwgZm9vZC5mcmVxLCBmb29kLnRmaWRmLCBmaWxlPSJkYXRhL3dvcmRlZmZlY3QucmRhdGEiKQ0Kc3RvcENsdXN0ZXIoY2x1c3QpICMgc3RvcCBwYXJhbGxlbCBwcm9jZXNzaW5nDQpgYGANCg0KPGJyPg0KPGJyPg0KPGJyPg0K