Data Import:
ebeer <- read_csv("ebeer.csv")
Introduction
RFM, recency (R), frequency
(F) and monetary value (M) are the
most often used database marketing metrics used to quantify customer
transaction history. RFM analysis segments customer into groups
according to these measures and relates these segments to the likelihood
of responding to a marketing offer. This notebook discusses the
measures, segmentation, usefulness for guiding marketing decisions, and
extensions to the basic model.
Inspecting the data
Each row is a customer. acctnum
is their id. We have
gender, Recency (the number of months since the last
purchase), Frequency (number of purchases),
M (average amount spent per purchase), first purchase
(number of months since first purchase), age, single, student, whether
they received a mailing, did they respond.
head(ebeer)
## # A tibble: 6 × 11
## acctnum gender R F M firstpur age_class single student mailing respmail
## <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 10001 1 30 10 35.7 50 3 1 1 1 0
## 2 10005 0 16 1 149 16 0 0 1 1 0
## 3 10010 0 12 1 123 12 0 1 0 1 0
## 4 10011 0 6 2 147 8 0 1 1 1 0
## 5 10014 1 6 3 96 18 0 0 1 1 1
## 6 10020 0 12 2 150. 14 0 1 1 1 0
Let’s look at the marketing variable: who gets a
mailing in this dataset?
table(ebeer$mailing)
##
## 0 1
## 5012 4952
How many people of those mailed respond?
ebeer %>%
group_by(mailing) %>%
summarise(mean = mean(respmail), n = n())
## # A tibble: 2 × 3
## mailing mean n
## <dbl> <dbl> <int>
## 1 0 NA 5012
## 2 1 0.124 4952
Binomial model for responses
The probability of observing \(s\)
people respond out of \(n\) people
mailed is described by a binomial distribution: \[P(s|n, p) = {n \choose s} p^{s}
(1-p)^{n-s}\]
The overall response rate (probability of response) is \(\hat{p}=\) 0.124, and the standard error is
0.005.
ci_low <- qnorm(0.025, mean=p_hat, sd=p_hat_se)
ci_high <- qnorm(0.975, mean=p_hat, sd=p_hat_se)
## The Response Rate is Between 0.115 and 0.134
The confidence interval is [0.115; 0.134].
Creating R, F and M segments separately.
Let’s look at the distribution of raw RFM variables.
ebeer$F <- as.numeric(ebeer$F)
par(mai=c(.9,.8,.2,.2))
hist(ebeer$R, main="Recency", xlab="Recency: # months since last purchase")
hist(ebeer$F, main="Frequency", xlab="Frequency: # purchases")
hist(ebeer$M, main="Monetary", xlab="Monetary: avg amount spent per purchase")
Let’s first create segments from each variable separately; we create
separate segments for R, F, and M. We sort them from largest to
smallest. Then we create \(n\) bins,
where \(n=5\)
We do this by creating quantiles, divide into 20% most recent, 20%
next most recent. We use the quantcut
function in package
rtools
.
ebeer$Rgroup <- quantcut(x = ebeer$R, q=5)
ebeer %>%
group_by(Rgroup) %>%
summarise(n=n(), mean_R=mean(R), sd_R=sd(R))
## # A tibble: 5 × 4
## Rgroup n mean_R sd_R
## <fct> <int> <dbl> <dbl>
## 1 [2,6] 2229 3.97 1.62
## 2 (6,10] 2069 9.22 0.975
## 3 (10,14] 2509 13.0 1.00
## 4 (14,16] 1231 16 0
## 5 (16,36] 1926 26.8 5.46
Now, let’s examine how response rate vary with the
recency groups we just created. We only want to look at the subset of
customer who were mailed, so we filter the dataset first
filter(mailing==1)
. We group by our just created 5 R
segments. And we calculate the mean of response,
mean(respmail)
.
respR <- ebeer %>%
filter(mailing==1) %>%
group_by(Rgroup) %>%
summarise(avg_respmail=mean(respmail), sd_respmail=sd(respmail), n=n())
respR
## # A tibble: 5 × 4
## Rgroup avg_respmail sd_respmail n
## <fct> <dbl> <dbl> <int>
## 1 [2,6] 0.126 0.332 1098
## 2 (6,10] 0.119 0.324 1041
## 3 (10,14] 0.122 0.328 1211
## 4 (14,16] 0.136 0.343 632
## 5 (16,36] 0.124 0.329 970
barplot(respR$avg_respmail~respR$Rgroup, main="response by Recency group", xlab="Recency Group", ylab="average response")
Full RFM analysis
Now do the full RFM analysis. Remember, the idea is that
- We first sort by R, create segments. (we already did this.)
- Within each R segment, we sort F and create RF segments.
- Within each RF segment, we sort M and create RFM segments.
The way to do this is slightly complicated; I would give you this
script in an exam or assignment. You would not have to code this up
yourselves. First, we change ebeer into data.table Within each R group,
we create F groups -> RF groups. Within each RF group, we create M
groups -> RFM groups
ntiles <- function(x, bins) {
quantiles = seq(from=0, to = 1, length.out=bins+1)
cut(ecdf(x)(x),breaks=quantiles, labels=F)
}
ebeer$Rgroup <- ntiles(ebeer$R, bins=5)
dt = data.table(ebeer)
nbins = 5
dt[, RFgroup := paste0(as.character(Rgroup), as.character(ntiles(F, bins = nbins))), by = c('Rgroup')]
dt[, RFMgroup := paste0(as.character(RFgroup), as.character(ntiles(M, bins = nbins))), by = c('RFgroup')]
# put it back to data.frame
ebeer = data.frame(dt)
# change it to a factor variable
ebeer$RFMgroup <- as.factor(ebeer$RFMgroup)
How many RFM groups do we get with this procedure?
length(unique(ebeer$RFMgroup))
## [1] 90
We have 90 RFM segments.
barplot(table(ebeer$RFMgroup), xlab = "RFM segments", ylab="frequency")
Response rate by RFM segment
Let’s make the response rate by segment.
# p = s_z/n_z
respRFM <- ebeer %>%
group_by(RFMgroup) %>%
summarise(n_resp= sum(respmail, na.rm = TRUE),
n_mail= sum(mailing, na.rm = TRUE)) %>%
mutate(resp_rate = n_resp/n_mail) %>%
arrange(desc(resp_rate)) #re-order from most to least, make picture better
respRFM
## # A tibble: 90 × 4
## RFMgroup n_resp n_mail resp_rate
## <fct> <dbl> <dbl> <dbl>
## 1 454 14 28 0.5
## 2 453 9 20 0.45
## 3 441 8 18 0.444
## 4 152 12 29 0.414
## 5 151 13 33 0.394
## 6 252 13 33 0.394
## 7 154 11 28 0.393
## 8 155 11 28 0.393
## 9 451 11 28 0.393
## 10 555 30 77 0.390
## # … with 80 more rows
Targeting using RFM analysis
Now let’s figure out which segments we should target. We want to
target segments that have a response rate above the breakeven point.
Remember the breakeven probability: \[
\bar{p}_{BE} = \frac{c}{m} = \frac{\textrm{cost}}{\textrm{margin}}
\]
c = 1.5
m = 50
brk = c/m
Our breakeven point is 0.03.
respRFM <- as.data.frame(respRFM)
bp <- barplot(respRFM[,4],
main="response by RFM group",
xlab="RFM Group", ylab="average response", xaxt="n")
axis(1, at = bp[,1], labels=respRFM[,1], cex.axis=0.7, las=2)
abline(h=brk)
text(85, brk, "breakeven", cex=1, pos=3, col="black")
How many segments are above the breakeven, and therefore targeted?
What segments are they? As a percentage of the total segments?
# how many segments above breakeven? which segments?
n_segments <- sum(respRFM$resp_rate >= brk)
# as a percentage of all segments
p_segments <- sum(respRFM$resp_rate >= brk) / length(unique(ebeer$RFMgroup))
## Optimal Number of Segments to target is 60 which is 66.7 % of total segments
Prediction of Rollout Customers
OK, now let’s apply this model to those customers who have not been
mailed, sometimes called the “rollout” sample. We use binary linear
regressions to predict. The \(\hat{\beta}_z\) will be the mean response
rate of each segment.
RFM_model <- lm(respmail ~ RFMgroup, data = ebeer)
Applying predictions to rollout data: ROI
We separate the rollout data (where there is no mailing) from
everything else. Then, we’ll score the new data, i.e., apply the
predictions of the model to the new data.
ebeer.rollout <- ebeer[is.na(ebeer$respmail), ]
##Prediction
ebeer.rollout$RFMpred <- predict(RFM_model, ebeer.rollout)
summary(ebeer.rollout$RFMpred)
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 0.000 0.000 0.085 0.125 0.185 0.500
The average prediction is the average response rate we found earlier.
So makes sense in terms of face validity.
We now have a probability of response to everyone in the rollout
data. How many customers in the rollout would get mailed? as a fraction
of the total, what would the profits and return on investment (ROI)
be?
# Total number of rollout customers with predicted response rates above breakeven
n_customers <- sum(ebeer.rollout$RFMpred >= brk)
# as a proportion of all rollout customers
p_customers <- sum(ebeer.rollout$RFMpred >= brk) / length(ebeer.rollout$RFMpred)
## Optimal Number of Segments to target is 3222 which is 64.3 % of total segments
# profit per customer
# if p > p_BE, expected profit = p*m - c || if p < p_BE, = 0
ebeer.rollout <- ebeer.rollout %>%
mutate(RFMprofit = case_when(RFMpred >= brk ~ RFMpred*m-c,
TRUE ~ 0))
# or pmax takes columnwise maximum (same as in L2)
#ebeer.rollout$RFMprofit <- pmax(ebeer.rollout$RFMpred *m - c, 0)
# sum over customers
sum_profit = sum(ebeer.rollout$RFMprofit)
# sum costs of targeting customers
ebeer.rollout$RFMcost <- ifelse(ebeer.rollout$RFMpred >= brk, c, 0)
sum_cost = sum(ebeer.rollout$RFMcost)
## ROI: 540 %
If we targeted everyone in the rollout group:
ebeer.rollout$all <-ebeer.rollout$RFMpred *m - c
sum_profit_all = sum(ebeer.rollout$all)
sum_cost_all = c*length(ebeer.rollout$RFMpred)
## ROI: 315 %
respRFM <- respRFM %>% mutate(n_nonresp = n_mail-n_resp) %>% relocate(n_nonresp, .after=n_resp)
Using a Bayesian approach
Right now we assume that these segments response rates are entirely
independent of each other. But if we make an assumption about the
distribution of response rates across segments, we could use that common
distribution to “borrow” information from the other segments.
par(mai=c(.9,.8,.2,.2))
hist(respRFM$resp_rate, density=10, breaks=20, main="Distribution of response rates across segments", xlab="segment-specific probability of response")
curve(dbeta(x, .3, 3), add = TRUE, type="l", col="gray")
Empirical Bayes
\[
\begin{array}{ccl}
P(s_z|n_z, a, b)& = & \displaystyle {n_z \choose s_z}
\frac{B(a+s_z,b+n_z-s_z)}{B(a,b)}
\end{array}
\] #### Prior We start by estimating the priors borrowing
information form other segments. We use MLE for
this:
Coef(fit)
## shape1 shape2
## 0.493 3.113
# make them a and b
a <- Coef(fit)[[1]]
b <- Coef(fit)[[2]]
Let’s plot this prior estimate against the data
par(mai=c(.9,.8,.2,.2))
hist(respRFM$resp_rate, density=10, breaks=20, main="Distribution of response rates across segments", xlab="segment-specific probability of response")
curve(dbeta(x, a, b), add = TRUE, type="l", col="red")
curve(dbeta(x, .3, 3), add = TRUE, type="l", col="blue")
Posterior Mean Response
# posterior mean response rate
post_mean_resp <- (a+respRFM$n_resp)/(a+b+respRFM$n_mail)
# add this as column to respRFM
respRFM <- cbind(respRFM, post_mean_resp)
#order from lowest to greatest
respRFM <- respRFM %>% arrange((resp_rate))
head(respRFM)
## RFMgroup n_resp n_nonresp n_mail resp_rate post_mean_resp
## 1 121 0 39 39 0 0.01157
## 2 122 0 48 48 0 0.00955
## 3 123 0 46 46 0 0.00994
## 4 124 0 45 45 0 0.01014
## 5 125 0 38 38 0 0.01185
## 6 131 0 43 43 0 0.01058
We plot this:
plot(respRFM$resp_rate, xaxt="n",col="red",xlab="RFM segments",ylab="response rate (x/n) and posterior mean response rate")
points(respRFM$post_mean_resp, col='blue')
legend('topleft',legend=c("estimate response rate", "posterior expected response rate"),col=c("red","blue"), pch=1)
axis(1, at = 1:90, labels=respRFM$RFMgroup, cex.axis=0.7, las=2)
abline(h=brk)
text(85, brk, "breakeven", cex=1, pos=3, col="black")
Are there any switches we would make using the posterior mean rather
than the actual mean to target segments?
## Using the posterior mean to target segments leads to 62 segments, whereas using the actual mean leads to 60
LS0tDQp0aXRsZTogIlR1dG9yaWFsIDI6IFJGTSBBbmFseXNpcyINCmF1dGhvcjogIkRhbmllbCBSZWRlbCINCmRhdGU6ICIyMDIyLTExLTAxIg0Kb3V0cHV0OiANCiAgaHRtbF9kb2N1bWVudDoNCiAgICB0b2M6IFRSVUUNCiAgICB0b2NfZmxvYXQ6IFRSVUUNCiAgICBjb2RlX2Rvd25sb2FkOiBUUlVFDQotLS0NCg0KYGBge3Igc2V0dXAsIGluY2x1ZGU9RkFMU0V9DQpybShsaXN0ID0gbHMoKSkNCg0KbGlicmFyeSh0aWR5dmVyc2UpDQpsaWJyYXJ5KGRhdGEudGFibGUpDQpsaWJyYXJ5KGthYmxlRXh0cmEpDQpsaWJyYXJ5KGd0b29scykNCmxpYnJhcnkoVkdBTSkNCmxpYnJhcnkocmVhZHIpDQoNCm9wdGlvbnMoInNjaXBlbiI9MTAwLCAiZGlnaXRzIj0zKQ0KYGBgDQoNCkRhdGEgSW1wb3J0Og0KDQpgYGB7ciBpbXBvcnQsIHdhcm5pbmc9RkFMU0UsIG1lc3NhZ2U9RkFMU0V9DQplYmVlciA8LSByZWFkX2NzdigiZWJlZXIuY3N2IikNCmBgYA0KDQojIyMgSW50cm9kdWN0aW9uDQoNCioqUkZNKiosIHJlY2VuY3kgKCoqUioqKSwgZnJlcXVlbmN5ICgqKkYqKikgYW5kIG1vbmV0YXJ5IHZhbHVlICgqKk0qKikgYXJlIHRoZSBtb3N0IG9mdGVuIHVzZWQgZGF0YWJhc2UgbWFya2V0aW5nIG1ldHJpY3MgdXNlZCB0byBxdWFudGlmeSBjdXN0b21lciB0cmFuc2FjdGlvbiBoaXN0b3J5LiBSRk0gYW5hbHlzaXMgc2VnbWVudHMgY3VzdG9tZXIgaW50byBncm91cHMgYWNjb3JkaW5nIHRvIHRoZXNlIG1lYXN1cmVzIGFuZCByZWxhdGVzIHRoZXNlIHNlZ21lbnRzIHRvIHRoZSBsaWtlbGlob29kIG9mIHJlc3BvbmRpbmcgdG8gYSBtYXJrZXRpbmcgb2ZmZXIuIFRoaXMgbm90ZWJvb2sgZGlzY3Vzc2VzIHRoZSBtZWFzdXJlcywgc2VnbWVudGF0aW9uLCB1c2VmdWxuZXNzIGZvciBndWlkaW5nIG1hcmtldGluZyBkZWNpc2lvbnMsIGFuZCBleHRlbnNpb25zIHRvIHRoZSBiYXNpYyBtb2RlbC4NCg0KIyMjIEluc3BlY3RpbmcgdGhlIGRhdGENCg0KRWFjaCByb3cgaXMgYSBjdXN0b21lci4gYGFjY3RudW1gIGlzIHRoZWlyIGlkLiBXZSBoYXZlIGdlbmRlciwgKipSZWNlbmN5KiogKHRoZSBudW1iZXIgb2YgbW9udGhzIHNpbmNlIHRoZSBsYXN0IHB1cmNoYXNlKSwgKipGcmVxdWVuY3kqKiAobnVtYmVyIG9mIHB1cmNoYXNlcyksICoqTSoqIChhdmVyYWdlIGFtb3VudCBzcGVudCBwZXIgcHVyY2hhc2UpLCBmaXJzdCBwdXJjaGFzZSAobnVtYmVyIG9mIG1vbnRocyBzaW5jZSBmaXJzdCBwdXJjaGFzZSksIGFnZSwgc2luZ2xlLCBzdHVkZW50LCB3aGV0aGVyIHRoZXkgcmVjZWl2ZWQgYSBtYWlsaW5nLCBkaWQgdGhleSByZXNwb25kLg0KDQpgYGB7cn0NCmhlYWQoZWJlZXIpDQpgYGANCg0KTGV0J3MgbG9vayBhdCB0aGUgKiptYXJrZXRpbmcgdmFyaWFibGUqKjogd2hvIGdldHMgYSBtYWlsaW5nIGluIHRoaXMgZGF0YXNldD8NCg0KYGBge3J9DQp0YWJsZShlYmVlciRtYWlsaW5nKQ0KYGBgDQoNCkhvdyBtYW55IHBlb3BsZSBvZiB0aG9zZSBtYWlsZWQgcmVzcG9uZD8NCg0KYGBge3J9DQplYmVlciAlPiUgDQogIGdyb3VwX2J5KG1haWxpbmcpICU+JSANCiAgc3VtbWFyaXNlKG1lYW4gPSBtZWFuKHJlc3BtYWlsKSwgbiA9IG4oKSkNCmBgYA0KDQojIyMgQmlub21pYWwgbW9kZWwgZm9yIHJlc3BvbnNlcw0KDQpUaGUgcHJvYmFiaWxpdHkgb2Ygb2JzZXJ2aW5nICRzJCBwZW9wbGUgcmVzcG9uZCBvdXQgb2YgJG4kIHBlb3BsZSBtYWlsZWQgaXMgZGVzY3JpYmVkIGJ5IGEgYmlub21pYWwgZGlzdHJpYnV0aW9uOiAkJFAoc3xuLCBwKSA9IHtuIFxjaG9vc2Ugc30gcF57c30gKDEtcClee24tc30kJA0KDQpgYGB7ciwgaW5jbHVkZT1GQUxTRX0NCnBfaGF0IDwtIG1lYW4oZWJlZXIkcmVzcG1haWxbZWJlZXIkbWFpbGluZz09MV0pDQpgYGANCg0KYGBge3IsIGluY2x1ZGU9RkFMU0V9DQpuID0gc3VtKGViZWVyJG1haWxpbmc9PTEpIA0KcF9oYXRfc2UgPSBzcXJ0KHBfaGF0KigxLXBfaGF0KS9uKSAjc3RhbmRhcmQgZXJyb3Igb2YgZXN0aW1hdGUgcA0KYGBgDQoNClRoZSBvdmVyYWxsIHJlc3BvbnNlIHJhdGUgKHByb2JhYmlsaXR5IG9mIHJlc3BvbnNlKSBpcyAkXGhhdHtwfT0kIGByIHBfaGF0YCwgYW5kIHRoZSBzdGFuZGFyZCBlcnJvciBpcyBgciBwX2hhdF9zZWAuDQoNCmBgYHtyIGNpfQ0KY2lfbG93IDwtIHFub3JtKDAuMDI1LCBtZWFuPXBfaGF0LCBzZD1wX2hhdF9zZSkgDQpjaV9oaWdoIDwtIHFub3JtKDAuOTc1LCBtZWFuPXBfaGF0LCBzZD1wX2hhdF9zZSkgDQoNCmBgYA0KDQpgYGB7ciwgZWNobz1GQUxTRX0NCmNhdCgiVGhlIFJlc3BvbnNlIFJhdGUgaXMgQmV0d2VlbiIsIGNpX2xvdywgImFuZCIsIGNpX2hpZ2gpDQpgYGANCg0KVGhlIGNvbmZpZGVuY2UgaW50ZXJ2YWwgaXMgW2ByIGNpX2xvd2A7IGByIGNpX2hpZ2hgXS4NCg0KIyMjIENyZWF0aW5nIFIsIEYgYW5kIE0gc2VnbWVudHMgc2VwYXJhdGVseS4NCg0KTGV0J3MgbG9vayBhdCB0aGUgZGlzdHJpYnV0aW9uIG9mIHJhdyBSRk0gdmFyaWFibGVzLg0KDQpgYGB7cn0NCmViZWVyJEYgPC0gYXMubnVtZXJpYyhlYmVlciRGKQ0KcGFyKG1haT1jKC45LC44LC4yLC4yKSkNCmhpc3QoZWJlZXIkUiwgbWFpbj0iUmVjZW5jeSIsIHhsYWI9IlJlY2VuY3k6ICMgbW9udGhzIHNpbmNlIGxhc3QgcHVyY2hhc2UiKQ0KaGlzdChlYmVlciRGLCBtYWluPSJGcmVxdWVuY3kiLCB4bGFiPSJGcmVxdWVuY3k6ICMgcHVyY2hhc2VzIikNCmhpc3QoZWJlZXIkTSwgbWFpbj0iTW9uZXRhcnkiLCB4bGFiPSJNb25ldGFyeTogYXZnIGFtb3VudCBzcGVudCBwZXIgcHVyY2hhc2UiKQ0KYGBgDQoNCkxldCdzIGZpcnN0IGNyZWF0ZSBzZWdtZW50cyBmcm9tIGVhY2ggdmFyaWFibGUgc2VwYXJhdGVseTsgd2UgY3JlYXRlIHNlcGFyYXRlIHNlZ21lbnRzIGZvciBSLCBGLCBhbmQgTS4gV2Ugc29ydCB0aGVtIGZyb20gbGFyZ2VzdCB0byBzbWFsbGVzdC4gVGhlbiB3ZSBjcmVhdGUgJG4kIGJpbnMsIHdoZXJlICRuPTUkDQoNCldlIGRvIHRoaXMgYnkgY3JlYXRpbmcgcXVhbnRpbGVzLCBkaXZpZGUgaW50byAyMCUgbW9zdCByZWNlbnQsIDIwJSBuZXh0IG1vc3QgcmVjZW50LiBXZSB1c2UgdGhlIGBxdWFudGN1dGAgZnVuY3Rpb24gaW4gcGFja2FnZSBgcnRvb2xzYC4NCg0KYGBge3J9DQplYmVlciRSZ3JvdXAgPC0gcXVhbnRjdXQoeCA9IGViZWVyJFIsIHE9NSkNCg0KZWJlZXIgJT4lIA0KICBncm91cF9ieShSZ3JvdXApICU+JSANCiAgc3VtbWFyaXNlKG49bigpLCBtZWFuX1I9bWVhbihSKSwgc2RfUj1zZChSKSkNCg0KYGBgDQoNCk5vdywgbGV0J3MgZXhhbWluZSBob3cgKipyZXNwb25zZSByYXRlKiogdmFyeSB3aXRoIHRoZSByZWNlbmN5IGdyb3VwcyB3ZSBqdXN0IGNyZWF0ZWQuIFdlIG9ubHkgd2FudCB0byBsb29rIGF0IHRoZSBzdWJzZXQgb2YgY3VzdG9tZXIgd2hvIHdlcmUgbWFpbGVkLCBzbyB3ZSBmaWx0ZXIgdGhlIGRhdGFzZXQgZmlyc3QgYGZpbHRlcihtYWlsaW5nPT0xKWAuIFdlIGdyb3VwIGJ5IG91ciBqdXN0IGNyZWF0ZWQgNSBSIHNlZ21lbnRzLiBBbmQgd2UgY2FsY3VsYXRlIHRoZSBtZWFuIG9mIHJlc3BvbnNlLCBgbWVhbihyZXNwbWFpbClgLg0KDQpgYGB7cn0NCnJlc3BSIDwtIGViZWVyICU+JSANCiAgZmlsdGVyKG1haWxpbmc9PTEpICU+JSANCiAgZ3JvdXBfYnkoUmdyb3VwKSAlPiUgDQogIHN1bW1hcmlzZShhdmdfcmVzcG1haWw9bWVhbihyZXNwbWFpbCksIHNkX3Jlc3BtYWlsPXNkKHJlc3BtYWlsKSwgbj1uKCkpDQpyZXNwUg0KYGBgDQoNCmBgYHtyfQ0KYmFycGxvdChyZXNwUiRhdmdfcmVzcG1haWx+cmVzcFIkUmdyb3VwLCBtYWluPSJyZXNwb25zZSBieSBSZWNlbmN5IGdyb3VwIiwgeGxhYj0iUmVjZW5jeSBHcm91cCIsIHlsYWI9ImF2ZXJhZ2UgcmVzcG9uc2UiKQ0KYGBgDQoNCiMjIyBGdWxsIFJGTSBhbmFseXNpcw0KDQpOb3cgZG8gdGhlIGZ1bGwgUkZNIGFuYWx5c2lzLiBSZW1lbWJlciwgdGhlIGlkZWEgaXMgdGhhdA0KDQoxLiAgV2UgZmlyc3Qgc29ydCBieSBSLCBjcmVhdGUgc2VnbWVudHMuICh3ZSBhbHJlYWR5IGRpZCB0aGlzLikNCjIuICBXaXRoaW4gZWFjaCBSIHNlZ21lbnQsIHdlIHNvcnQgRiBhbmQgY3JlYXRlIFJGIHNlZ21lbnRzLg0KMy4gIFdpdGhpbiBlYWNoIFJGIHNlZ21lbnQsIHdlIHNvcnQgTSBhbmQgY3JlYXRlIFJGTSBzZWdtZW50cy4NCg0KVGhlIHdheSB0byBkbyB0aGlzIGlzIHNsaWdodGx5IGNvbXBsaWNhdGVkOyBJIHdvdWxkIGdpdmUgeW91IHRoaXMgc2NyaXB0IGluIGFuIGV4YW0gb3IgYXNzaWdubWVudC4gWW91IHdvdWxkIG5vdCBoYXZlIHRvIGNvZGUgdGhpcyB1cCB5b3Vyc2VsdmVzLiBGaXJzdCwgd2UgY2hhbmdlIGViZWVyIGludG8gZGF0YS50YWJsZSBXaXRoaW4gZWFjaCBSIGdyb3VwLCB3ZSBjcmVhdGUgRiBncm91cHMgLVw+IFJGIGdyb3Vwcy4gV2l0aGluIGVhY2ggUkYgZ3JvdXAsIHdlIGNyZWF0ZSBNIGdyb3VwcyAtXD4gUkZNIGdyb3Vwcw0KDQpgYGB7ciwgY2FjaGU9VFJVRX0NCg0KbnRpbGVzIDwtIGZ1bmN0aW9uKHgsIGJpbnMpIHsNCiAgcXVhbnRpbGVzID0gc2VxKGZyb209MCwgdG8gPSAxLCBsZW5ndGgub3V0PWJpbnMrMSkNCiAgY3V0KGVjZGYoeCkoeCksYnJlYWtzPXF1YW50aWxlcywgbGFiZWxzPUYpDQp9DQoNCmViZWVyJFJncm91cCA8LSBudGlsZXMoZWJlZXIkUiwgYmlucz01KSAgDQoNCg0KZHQgPSBkYXRhLnRhYmxlKGViZWVyKQ0KbmJpbnMgPSA1DQpkdFssIFJGZ3JvdXAgOj0gcGFzdGUwKGFzLmNoYXJhY3RlcihSZ3JvdXApLCBhcy5jaGFyYWN0ZXIobnRpbGVzKEYsIGJpbnMgPSBuYmlucykpKSwgYnkgPSBjKCdSZ3JvdXAnKV0NCmR0WywgUkZNZ3JvdXAgOj0gcGFzdGUwKGFzLmNoYXJhY3RlcihSRmdyb3VwKSwgYXMuY2hhcmFjdGVyKG50aWxlcyhNLCBiaW5zID0gbmJpbnMpKSksIGJ5ID0gYygnUkZncm91cCcpXQ0KDQojIHB1dCBpdCBiYWNrIHRvIGRhdGEuZnJhbWUNCmViZWVyID0gZGF0YS5mcmFtZShkdCkNCg0KIyBjaGFuZ2UgaXQgdG8gYSBmYWN0b3IgdmFyaWFibGUNCmViZWVyJFJGTWdyb3VwIDwtIGFzLmZhY3RvcihlYmVlciRSRk1ncm91cCkNCg0KYGBgDQoNCkhvdyBtYW55IFJGTSBncm91cHMgZG8gd2UgZ2V0IHdpdGggdGhpcyBwcm9jZWR1cmU/DQoNCmBgYHtyfQ0KbGVuZ3RoKHVuaXF1ZShlYmVlciRSRk1ncm91cCkpDQpgYGANCg0KV2UgaGF2ZSBgciBsZW5ndGgodW5pcXVlKGViZWVyJFJGTWdyb3VwKSlgIFJGTSBzZWdtZW50cy4NCg0KYGBge3J9DQpiYXJwbG90KHRhYmxlKGViZWVyJFJGTWdyb3VwKSwgeGxhYiA9ICJSRk0gc2VnbWVudHMiLCB5bGFiPSJmcmVxdWVuY3kiKQ0KYGBgDQoNCiMjIyBSZXNwb25zZSByYXRlIGJ5IFJGTSBzZWdtZW50DQoNCkxldCdzIG1ha2UgdGhlIHJlc3BvbnNlIHJhdGUgYnkgc2VnbWVudC4NCg0KYGBge3J9DQojIHAgPSBzX3ovbl96DQpyZXNwUkZNIDwtIGViZWVyICU+JSANCiAgZ3JvdXBfYnkoUkZNZ3JvdXApICU+JSANCiAgc3VtbWFyaXNlKG5fcmVzcD0gc3VtKHJlc3BtYWlsLCBuYS5ybSA9IFRSVUUpLCANCiAgICAgICAgICAgIG5fbWFpbD0gc3VtKG1haWxpbmcsIG5hLnJtID0gVFJVRSkpICU+JSANCiAgbXV0YXRlKHJlc3BfcmF0ZSA9IG5fcmVzcC9uX21haWwpICU+JSANCiAgYXJyYW5nZShkZXNjKHJlc3BfcmF0ZSkpICNyZS1vcmRlciBmcm9tIG1vc3QgdG8gbGVhc3QsIG1ha2UgcGljdHVyZSBiZXR0ZXINCnJlc3BSRk0NCmBgYA0KDQojIyMgVGFyZ2V0aW5nIHVzaW5nIFJGTSBhbmFseXNpcw0KDQpOb3cgbGV0J3MgZmlndXJlIG91dCB3aGljaCBzZWdtZW50cyB3ZSBzaG91bGQgdGFyZ2V0LiBXZSB3YW50IHRvIHRhcmdldCBzZWdtZW50cyB0aGF0IGhhdmUgYSByZXNwb25zZSByYXRlIGFib3ZlIHRoZSBicmVha2V2ZW4gcG9pbnQuIFJlbWVtYmVyIHRoZSBicmVha2V2ZW4gcHJvYmFiaWxpdHk6ICQkDQpcYmFye3B9X3tCRX0gPSBcZnJhY3tjfXttfSA9IFxmcmFje1x0ZXh0cm17Y29zdH19e1x0ZXh0cm17bWFyZ2lufX0gDQokJA0KDQpgYGB7ciBicmVha2V2ZW59DQpjID0gMS41DQptID0gNTANCmJyayA9IGMvbQ0KYGBgDQoNCk91ciBicmVha2V2ZW4gcG9pbnQgaXMgYHIgYnJrYC4NCg0KYGBge3J9DQpyZXNwUkZNIDwtIGFzLmRhdGEuZnJhbWUocmVzcFJGTSkNCg0KYnAgPC0gYmFycGxvdChyZXNwUkZNWyw0XSwgDQogICAgICAgICAgICAgIG1haW49InJlc3BvbnNlIGJ5IFJGTSBncm91cCIsIA0KICAgICAgICAgICAgICB4bGFiPSJSRk0gR3JvdXAiLCB5bGFiPSJhdmVyYWdlIHJlc3BvbnNlIiwgeGF4dD0ibiIpDQpheGlzKDEsIGF0ID0gYnBbLDFdLCBsYWJlbHM9cmVzcFJGTVssMV0sIGNleC5heGlzPTAuNywgbGFzPTIpDQoNCmFibGluZShoPWJyaykNCnRleHQoODUsIGJyaywgImJyZWFrZXZlbiIsIGNleD0xLCBwb3M9MywgY29sPSJibGFjayIpDQoNCmBgYA0KDQpIb3cgbWFueSBzZWdtZW50cyBhcmUgYWJvdmUgdGhlIGJyZWFrZXZlbiwgYW5kIHRoZXJlZm9yZSB0YXJnZXRlZD8gV2hhdCBzZWdtZW50cyBhcmUgdGhleT8gQXMgYSBwZXJjZW50YWdlIG9mIHRoZSB0b3RhbCBzZWdtZW50cz8NCg0KYGBge3J9DQojIGhvdyBtYW55IHNlZ21lbnRzIGFib3ZlIGJyZWFrZXZlbj8gd2hpY2ggc2VnbWVudHM/DQpuX3NlZ21lbnRzIDwtIHN1bShyZXNwUkZNJHJlc3BfcmF0ZSA+PSBicmspDQojIGFzIGEgcGVyY2VudGFnZSBvZiBhbGwgc2VnbWVudHMNCnBfc2VnbWVudHMgPC0gc3VtKHJlc3BSRk0kcmVzcF9yYXRlID49IGJyaykgLyBsZW5ndGgodW5pcXVlKGViZWVyJFJGTWdyb3VwKSkNCmBgYA0KDQpgYGB7ciwgZWNobz1GQUxTRX0NCmNhdCgiT3B0aW1hbCBOdW1iZXIgb2YgU2VnbWVudHMgdG8gdGFyZ2V0IGlzIiwgbl9zZWdtZW50cywgIndoaWNoIGlzIiwgMTAwKnBfc2VnbWVudHMsICIlIG9mIHRvdGFsIHNlZ21lbnRzIikNCmBgYA0KDQojIyMjIFByZWRpY3Rpb24gb2YgUm9sbG91dCBDdXN0b21lcnMNCg0KT0ssIG5vdyBsZXQncyBhcHBseSB0aGlzIG1vZGVsIHRvIHRob3NlIGN1c3RvbWVycyB3aG8gaGF2ZSBub3QgYmVlbiBtYWlsZWQsIHNvbWV0aW1lcyBjYWxsZWQgdGhlICJyb2xsb3V0IiBzYW1wbGUuIFdlIHVzZSBiaW5hcnkgbGluZWFyIHJlZ3Jlc3Npb25zIHRvIHByZWRpY3QuIFRoZSAkXGhhdHtcYmV0YX1feiQgd2lsbCBiZSB0aGUgbWVhbiByZXNwb25zZSByYXRlIG9mIGVhY2ggc2VnbWVudC4NCg0KYGBge3J9DQpSRk1fbW9kZWwgPC0gbG0ocmVzcG1haWwgfiBSRk1ncm91cCwgZGF0YSA9IGViZWVyKQ0KYGBgDQoNCiMjIyBBcHBseWluZyBwcmVkaWN0aW9ucyB0byByb2xsb3V0IGRhdGE6IFJPSQ0KDQpXZSBzZXBhcmF0ZSB0aGUgcm9sbG91dCBkYXRhICh3aGVyZSB0aGVyZSBpcyBubyBtYWlsaW5nKSBmcm9tIGV2ZXJ5dGhpbmcgZWxzZS4gVGhlbiwgd2UnbGwgc2NvcmUgdGhlIG5ldyBkYXRhLCBpLmUuLCBhcHBseSB0aGUgcHJlZGljdGlvbnMgb2YgdGhlIG1vZGVsIHRvIHRoZSBuZXcgZGF0YS4NCg0KYGBge3J9DQplYmVlci5yb2xsb3V0IDwtIGViZWVyW2lzLm5hKGViZWVyJHJlc3BtYWlsKSwgXQ0KDQojI1ByZWRpY3Rpb24NCmViZWVyLnJvbGxvdXQkUkZNcHJlZCA8LSBwcmVkaWN0KFJGTV9tb2RlbCwgZWJlZXIucm9sbG91dCkNCg0Kc3VtbWFyeShlYmVlci5yb2xsb3V0JFJGTXByZWQpDQoNCg0KYGBgDQoNClRoZSBhdmVyYWdlIHByZWRpY3Rpb24gaXMgdGhlIGF2ZXJhZ2UgcmVzcG9uc2UgcmF0ZSB3ZSBmb3VuZCBlYXJsaWVyLiBTbyBtYWtlcyBzZW5zZSBpbiB0ZXJtcyBvZiBmYWNlIHZhbGlkaXR5Lg0KDQpXZSBub3cgaGF2ZSBhIHByb2JhYmlsaXR5IG9mIHJlc3BvbnNlIHRvIGV2ZXJ5b25lIGluIHRoZSByb2xsb3V0IGRhdGEuIEhvdyBtYW55IGN1c3RvbWVycyBpbiB0aGUgcm9sbG91dCB3b3VsZCBnZXQgbWFpbGVkPyBhcyBhIGZyYWN0aW9uIG9mIHRoZSB0b3RhbCwgd2hhdCB3b3VsZCB0aGUgcHJvZml0cyBhbmQgcmV0dXJuIG9uIGludmVzdG1lbnQgKFJPSSkgYmU/DQoNCmBgYHtyfQ0KIyBUb3RhbCBudW1iZXIgb2Ygcm9sbG91dCBjdXN0b21lcnMgd2l0aCBwcmVkaWN0ZWQgcmVzcG9uc2UgcmF0ZXMgYWJvdmUgYnJlYWtldmVuDQpuX2N1c3RvbWVycyA8LSBzdW0oZWJlZXIucm9sbG91dCRSRk1wcmVkID49IGJyaykNCiMgYXMgYSBwcm9wb3J0aW9uIG9mIGFsbCByb2xsb3V0IGN1c3RvbWVycw0KcF9jdXN0b21lcnMgPC0gc3VtKGViZWVyLnJvbGxvdXQkUkZNcHJlZCA+PSBicmspIC8gbGVuZ3RoKGViZWVyLnJvbGxvdXQkUkZNcHJlZCkNCmBgYA0KDQpgYGB7ciwgZWNobz1GQUxTRX0NCg0KY2F0KCJPcHRpbWFsIE51bWJlciBvZiBTZWdtZW50cyB0byB0YXJnZXQgaXMiLCBuX2N1c3RvbWVycywgIndoaWNoIGlzIiwgMTAwKnBfY3VzdG9tZXJzLCAiJSBvZiB0b3RhbCBzZWdtZW50cyIpDQpgYGANCg0KYGBge3J9DQojIHByb2ZpdCBwZXIgY3VzdG9tZXINCiMgaWYgcCA+IHBfQkUsIGV4cGVjdGVkIHByb2ZpdCA9IHAqbSAtIGMgIHx8ICBpZiBwIDwgcF9CRSwgPSAwDQplYmVlci5yb2xsb3V0IDwtIGViZWVyLnJvbGxvdXQgJT4lIA0KICBtdXRhdGUoUkZNcHJvZml0ID0gY2FzZV93aGVuKFJGTXByZWQgPj0gYnJrIH4gUkZNcHJlZCptLWMsIA0KICAgIFRSVUUgfiAwKSkNCiMgb3IgcG1heCB0YWtlcyBjb2x1bW53aXNlIG1heGltdW0gKHNhbWUgYXMgaW4gTDIpDQojZWJlZXIucm9sbG91dCRSRk1wcm9maXQgPC0gcG1heChlYmVlci5yb2xsb3V0JFJGTXByZWQgKm0gLSBjLCAwKQ0KDQojIHN1bSBvdmVyIGN1c3RvbWVycw0Kc3VtX3Byb2ZpdCA9IHN1bShlYmVlci5yb2xsb3V0JFJGTXByb2ZpdCkNCg0KIyBzdW0gY29zdHMgb2YgdGFyZ2V0aW5nIGN1c3RvbWVycw0KZWJlZXIucm9sbG91dCRSRk1jb3N0IDwtIGlmZWxzZShlYmVlci5yb2xsb3V0JFJGTXByZWQgPj0gYnJrLCBjLCAwKQ0KDQpzdW1fY29zdCA9IHN1bShlYmVlci5yb2xsb3V0JFJGTWNvc3QpDQoNCmBgYA0KDQpgYGB7ciwgZWNobz1GQUxTRX0NCiMgd2hhdCBhYm91dCB0aGUgcmV0dXJuIG9uIGludmVzdG1lbnQgUk9JPw0KY2F0KCJST0k6Iiwgc3VtX3Byb2ZpdCAvIHN1bV9jb3N0KjEwMCwgIiUiKQ0KYGBgDQoNCklmIHdlIHRhcmdldGVkIGV2ZXJ5b25lIGluIHRoZSByb2xsb3V0IGdyb3VwOg0KDQpgYGB7cn0NCmViZWVyLnJvbGxvdXQkYWxsIDwtZWJlZXIucm9sbG91dCRSRk1wcmVkICptIC0gYw0Kc3VtX3Byb2ZpdF9hbGwgPSBzdW0oZWJlZXIucm9sbG91dCRhbGwpDQpzdW1fY29zdF9hbGwgPSBjKmxlbmd0aChlYmVlci5yb2xsb3V0JFJGTXByZWQpDQpgYGANCg0KYGBge3IsIGVjaG89RkFMU0V9DQpjYXQoIlJPSToiLCBzdW1fcHJvZml0X2FsbCAvIHN1bV9jb3N0X2FsbCoxMDAsICIlIikNCmBgYA0KDQpgYGB7cn0NCnJlc3BSRk0gPC0gcmVzcFJGTSAlPiUgbXV0YXRlKG5fbm9ucmVzcCA9IG5fbWFpbC1uX3Jlc3ApICU+JSByZWxvY2F0ZShuX25vbnJlc3AsIC5hZnRlcj1uX3Jlc3ApDQpgYGANCg0KIyMjIFVzaW5nIGEgQmF5ZXNpYW4gYXBwcm9hY2gNCg0KUmlnaHQgbm93IHdlIGFzc3VtZSB0aGF0IHRoZXNlIHNlZ21lbnRzIHJlc3BvbnNlIHJhdGVzIGFyZSBlbnRpcmVseSBpbmRlcGVuZGVudCBvZiBlYWNoIG90aGVyLiBCdXQgaWYgd2UgbWFrZSBhbiBhc3N1bXB0aW9uIGFib3V0IHRoZSBkaXN0cmlidXRpb24gb2YgcmVzcG9uc2UgcmF0ZXMgYWNyb3NzIHNlZ21lbnRzLCB3ZSBjb3VsZCB1c2UgdGhhdCBjb21tb24gZGlzdHJpYnV0aW9uIHRvICJib3Jyb3ciIGluZm9ybWF0aW9uIGZyb20gdGhlIG90aGVyIHNlZ21lbnRzLg0KDQpgYGB7cn0NCnBhcihtYWk9YyguOSwuOCwuMiwuMikpDQpoaXN0KHJlc3BSRk0kcmVzcF9yYXRlLCBkZW5zaXR5PTEwLCBicmVha3M9MjAsIG1haW49IkRpc3RyaWJ1dGlvbiBvZiByZXNwb25zZSByYXRlcyBhY3Jvc3Mgc2VnbWVudHMiLCB4bGFiPSJzZWdtZW50LXNwZWNpZmljIHByb2JhYmlsaXR5IG9mIHJlc3BvbnNlIikNCmN1cnZlKGRiZXRhKHgsIC4zLCAzKSwgYWRkID0gVFJVRSwgIHR5cGU9ImwiLCBjb2w9ImdyYXkiKQ0KYGBgDQoNCiMjIyBFbXBpcmljYWwgQmF5ZXMNCg0KJCQgDQpcYmVnaW57YXJyYXl9e2NjbH0NClAoc196fG5feiwgYSwgYikmICA9ICYgXGRpc3BsYXlzdHlsZSB7bl96IFxjaG9vc2Ugc196fSBcZnJhY3tCKGErc196LGIrbl96LXNfeil9e0IoYSxiKX0gDQpcZW5ke2FycmF5fQ0KJCQgXCMjIyMgUHJpb3IgV2Ugc3RhcnQgYnkgZXN0aW1hdGluZyB0aGUgcHJpb3JzIGJvcnJvd2luZyBpbmZvcm1hdGlvbiBmb3JtIG90aGVyIHNlZ21lbnRzLiBXZSB1c2UgKipNTEUqKiBmb3IgdGhpczoNCg0KYGBge3IsIHdhcm5pbmc9RkFMU0UsIGluY2x1ZGU9RkFMU0V9DQojaW5zdGFsbC5wYWNrYWdlcygiVkdBTSIpDQojIGEgPSBhICsgc196DQojIGIgPSBiICsgKG5fei1zX3opDQpmaXQgPC0gdmdsbShjYmluZChyZXNwUkZNJG5fcmVzcCxyZXNwUkZNJG5fbm9ucmVzcCkgfiAxLCBiZXRhYmlub21pYWxmZiwgdHJhY2U9VFJVRSkNCg0KYGBgDQoNCmBgYHtyfQ0KQ29lZihmaXQpDQoNCiMgbWFrZSB0aGVtIGEgYW5kIGINCmEgPC0gQ29lZihmaXQpW1sxXV0NCmIgPC0gQ29lZihmaXQpW1syXV0NCmBgYA0KDQpMZXQncyBwbG90IHRoaXMgcHJpb3IgZXN0aW1hdGUgYWdhaW5zdCB0aGUgZGF0YQ0KDQpgYGB7cn0NCnBhcihtYWk9YyguOSwuOCwuMiwuMikpDQpoaXN0KHJlc3BSRk0kcmVzcF9yYXRlLCBkZW5zaXR5PTEwLCBicmVha3M9MjAsIG1haW49IkRpc3RyaWJ1dGlvbiBvZiByZXNwb25zZSByYXRlcyBhY3Jvc3Mgc2VnbWVudHMiLCB4bGFiPSJzZWdtZW50LXNwZWNpZmljIHByb2JhYmlsaXR5IG9mIHJlc3BvbnNlIikNCmN1cnZlKGRiZXRhKHgsIGEsIGIpLCBhZGQgPSBUUlVFLCAgdHlwZT0ibCIsIGNvbD0icmVkIikNCmN1cnZlKGRiZXRhKHgsIC4zLCAzKSwgYWRkID0gVFJVRSwgIHR5cGU9ImwiLCBjb2w9ImJsdWUiKQ0KYGBgDQoNCiMjIyMgUG9zdGVyaW9yIE1lYW4gUmVzcG9uc2UNCg0KYGBge3J9DQojIHBvc3RlcmlvciBtZWFuIHJlc3BvbnNlIHJhdGUNCnBvc3RfbWVhbl9yZXNwIDwtIChhK3Jlc3BSRk0kbl9yZXNwKS8oYStiK3Jlc3BSRk0kbl9tYWlsKQ0KICAgICAgICAgICAgIA0KIyBhZGQgdGhpcyBhcyBjb2x1bW4gdG8gcmVzcFJGTQ0KcmVzcFJGTSA8LSBjYmluZChyZXNwUkZNLCBwb3N0X21lYW5fcmVzcCkNCg0KI29yZGVyIGZyb20gbG93ZXN0IHRvIGdyZWF0ZXN0DQpyZXNwUkZNIDwtIHJlc3BSRk0gJT4lIGFycmFuZ2UoKHJlc3BfcmF0ZSkpDQoNCmhlYWQocmVzcFJGTSkNCmBgYA0KDQpXZSBwbG90IHRoaXM6DQoNCmBgYHtyfQ0KcGxvdChyZXNwUkZNJHJlc3BfcmF0ZSwgeGF4dD0ibiIsY29sPSJyZWQiLHhsYWI9IlJGTSBzZWdtZW50cyIseWxhYj0icmVzcG9uc2UgcmF0ZSAoeC9uKSBhbmQgcG9zdGVyaW9yIG1lYW4gcmVzcG9uc2UgcmF0ZSIpDQpwb2ludHMocmVzcFJGTSRwb3N0X21lYW5fcmVzcCwgY29sPSdibHVlJykNCmxlZ2VuZCgndG9wbGVmdCcsbGVnZW5kPWMoImVzdGltYXRlIHJlc3BvbnNlIHJhdGUiLCAicG9zdGVyaW9yIGV4cGVjdGVkIHJlc3BvbnNlIHJhdGUiKSxjb2w9YygicmVkIiwiYmx1ZSIpLCBwY2g9MSkNCmF4aXMoMSwgYXQgPSAxOjkwLCBsYWJlbHM9cmVzcFJGTSRSRk1ncm91cCwgY2V4LmF4aXM9MC43LCBsYXM9MikNCmFibGluZShoPWJyaykNCnRleHQoODUsIGJyaywgImJyZWFrZXZlbiIsIGNleD0xLCBwb3M9MywgY29sPSJibGFjayIpDQoNCmBgYA0KDQpBcmUgdGhlcmUgYW55IHN3aXRjaGVzIHdlIHdvdWxkIG1ha2UgdXNpbmcgdGhlIHBvc3RlcmlvciBtZWFuIHJhdGhlciB0aGFuIHRoZSBhY3R1YWwgbWVhbiB0byB0YXJnZXQgc2VnbWVudHM/DQoNCmBgYHtyLCBlY2hvPUZBTFNFfQ0KY2F0KCJVc2luZyB0aGUgcG9zdGVyaW9yIG1lYW4gdG8gdGFyZ2V0IHNlZ21lbnRzIGxlYWRzIHRvIiwgc3VtKHJlc3BSRk0kcG9zdF9tZWFuX3Jlc3A+PWJyayksICJzZWdtZW50cywgd2hlcmVhcyB1c2luZyB0aGUgYWN0dWFsIG1lYW4gbGVhZHMgdG8iLCBzdW0ocmVzcFJGTSRyZXNwX3JhdGU+PWJyaykgKQ0KYGBgDQo=