Goals:

  • Handcode R-learner

  • Illustrate use of R-learner and DR-learner


DGP

Consider a DGP with linear heterogeneous treatment effects, but nonlinear propensity score and outcome:

  • \(p=10\) independent covariates \(X_1,...,X_k,...,X_{10}\) drawn from a uniform distribution: \(X_k \sim uniform(-\pi,\pi)\)

  • The treatment model is \(W \sim Bernoulli(\underbrace{\Phi(sin(X_1))}_{e(X)})\), where \(\Phi(\cdot)\) is the standard normal cumulative density function

  • The potential outcome model of the controls is \(Y(0) = \underbrace{sin(X_1)}_{m_0(X)} + \varepsilon\), with \(\varepsilon \sim N(0,1)\)

  • The CATE function is a linear function of the first three covariates \(\tau(X) = \underbrace{0.3}_{\rho_1} X_1 + \underbrace{0.2}_{\rho_2} X_2 + \underbrace{0.1}_{\rho_3} X_3\)

  • The potential outcome model of the treated is \(Y(1) = m_0(X) + \tau(X) + \varepsilon\), with \(\varepsilon \sim N(0,1)\)

We draw a sample of 1000 observations to begin with:

if (!require("grf")) install.packages("grf", dependencies = TRUE); library(grf)
if (!require("tidyverse")) install.packages("tidyverse", dependencies = TRUE); library(tidyverse)
if (!require("glmnet")) install.packages("glmnet", dependencies = TRUE); library(glmnet)
if (!require("psych")) install.packages("psych", dependencies = TRUE); library(psych)
if (!require("causalDML")) {
  if (!require("devtools")) install.packages("devtools", dependencies = TRUE); library(devtools)
  install_github(repo="MCKnaus/causalDML") 
}; library(causalDML)
if (!require("rlearner")) {
  if (!require("devtools")) install.packages("devtools", dependencies = TRUE); library(devtools)
  install_github("xnie/rlearner")
}; library(rlearner)

set.seed(1234)

# Set parameters
n = 1000
p = 10

# Correct parameters
rho = c(0.3,0.2,0.1,rep(0,p-3))

# Draw sample
x = matrix(runif(n*p,-pi,pi),ncol=p)
e = function(x){pnorm(sin(x))}
m0 = function(x){sin(x)}
tau = x %*% rho
w = rbinom(n,1,e(x[,1]))
y = m0(x[,1]) + w*tau + rnorm(n,0,1)


Handcoded R-learner with OLS last step

For illustration purposes we handcode an R-learner with cross-fitted nuisance parameters via honest Random Forest and heterogeneity estimation via OLS. This should work well because the nuisance parameters are nonlinear and heterogeneity is actually linear.

First, we get the nuisance parameters \(e(X)=E[W|X]\) and \(m(X)=E[Y|X]\) via self-tuned honest Random Forest. We handcode 2-fold cross-validation in the familiar way:

# R-learner with OLS last stage
# 2-fold cross-fitting
mhat = ehat = rep(NA,n)
# Draw random indices for sample 1
index_s1 = sample(1:n,n/2)
# Create S1
x1 = x[index_s1,]
w1 = w[index_s1]
y1 = y[index_s1]
# Create sample 2 with those not in S1
x2 = x[-index_s1,]
w2 = w[-index_s1]
y2 = y[-index_s1]
# Model in S1, predict in S2
rf = regression_forest(x1,w1,tune.parameters = "all")
ehat[-index_s1] = predict(rf,newdata=x2)$predictions
rf = regression_forest(x1,y1,tune.parameters = "all")
mhat[-index_s1] = predict(rf,newdata=x2)$predictions
# Model in S2, predict in S1
rf = regression_forest(x2,w2,tune.parameters = "all")
ehat[index_s1] = predict(rf,newdata=x1)$predictions
rf = regression_forest(x2,y2,tune.parameters = "all")
mhat[index_s1] = predict(rf,newdata=x1)$predictions

We have now two ways to implement the R-learner


1. Modify the covariates

Recall that we can rewrite the R-learner as minimizing a least squares problem with outcome residual as pseudo-outcome and modified covariates:

\[\hat{\beta}^{rl} = argmin_{\beta} \sum_{i=1}^N \big(Y_i - \hat{m}(X_i) - X_i^* \beta \big)^2\]

where \(X_i^* = X_i(W_i - \hat{e}(X_i))\) are the modified/pseudo-covariates.

Note that \(X_i\) includes the constant such that the first column of the modified covariates equals the treatment residuals and we run a regression without a “real” constant of ones:

# Create residuals
res_y = y-mhat
res_w = w-ehat

# Modify covariates (multiply each column including constant with residual)
x_wc = cbind(rep(1,n),x)
colnames(x_wc) = c("Intercept",paste0("X",1:p))
xstar = x_wc * res_w
# Regress outcome residual on modified covariates
summary(lm(res_y ~ 0 + xstar))

Call:
lm(formula = res_y ~ 0 + xstar)

Residuals:
    Min      1Q  Median      3Q     Max 
-2.8599 -0.6829  0.0280  0.6451  3.1853 

Coefficients:
                Estimate Std. Error t value Pr(>|t|)    
xstarIntercept  0.088067   0.071056   1.239   0.2155    
xstarX1         0.226436   0.038016   5.956 3.58e-09 ***
xstarX2         0.210385   0.039627   5.309 1.36e-07 ***
xstarX3         0.083184   0.039557   2.103   0.0357 *  
xstarX4         0.007685   0.039217   0.196   0.8447    
xstarX5         0.026108   0.038243   0.683   0.4950    
xstarX6         0.083009   0.039672   2.092   0.0367 *  
xstarX7        -0.018537   0.039428  -0.470   0.6384    
xstarX8         0.034299   0.039734   0.863   0.3882    
xstarX9        -0.031037   0.040560  -0.765   0.4443    
xstarX10        0.023241   0.038538   0.603   0.5466    
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

Residual standard error: 0.9976 on 989 degrees of freedom
Multiple R-squared:  0.07587,   Adjusted R-squared:  0.06559 
F-statistic: 7.381 on 11 and 989 DF,  p-value: 3.027e-12

The estimated coefficients are relatively close to their true values.


2. Pseudo-outcome and weights

The more generic alternative is to use the unmodified covariates in a weighted regression with pseudo outcomes: \[\hat{\beta}^{rl} = argmin_{\beta} \sum_{i=1}^N \underbrace{(W_i - \hat{e}(X_i))^2}_{\text{weight}} \left(\underbrace{\frac{Y_i - \hat{m}(X_i)}{W_i - \hat{e}(X_i)}}_{\text{pseudo-outcome}} - X_i'\beta \right)^2\]

# Create pseudo-outcome (outcome res divided by treatment res)
pseudo_rl = res_y / res_w

# Create weights
weights_rl = res_w^2

# Weighted regression of pseudo-outcome on covariates
rols_fit = lm(pseudo_rl ~ x, weights=weights_rl)
summary(rols_fit)

Call:
lm(formula = pseudo_rl ~ x, weights = weights_rl)

Weighted Residuals:
    Min      1Q  Median      3Q     Max 
-3.1853 -0.6460  0.0353  0.6870  2.7264 

Coefficients:
             Estimate Std. Error t value Pr(>|t|)    
(Intercept)  0.088067   0.071056   1.239   0.2155    
x1           0.226436   0.038016   5.956 3.58e-09 ***
x2           0.210385   0.039627   5.309 1.36e-07 ***
x3           0.083184   0.039557   2.103   0.0357 *  
x4           0.007685   0.039217   0.196   0.8447    
x5           0.026108   0.038243   0.683   0.4950    
x6           0.083009   0.039672   2.092   0.0367 *  
x7          -0.018537   0.039428  -0.470   0.6384    
x8           0.034299   0.039734   0.863   0.3882    
x9          -0.031037   0.040560  -0.765   0.4443    
x10          0.023241   0.038538   0.603   0.5466    
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

Residual standard error: 0.9976 on 989 degrees of freedom
Multiple R-squared:  0.07497,   Adjusted R-squared:  0.06562 
F-statistic: 8.016 on 10 and 989 DF,  p-value: 1.632e-12
r_ols_est = predict(rols_fit)

This produces the same results as the modified covariate as the equality of all coefficients shows:

# test if all values are equal
all.equal(as.numeric(rols_fit$coefficients), as.numeric(lm(res_y ~ 0 + xstar)$coefficients))
[1] TRUE

We store the fitted values \(\hat{\tau}(x) = x'\hat{\beta}^{rl}\) as estimates of the CATEs to compare them later with the truth and other methods.

# Estimate CATEs
r_ols_est = predict(rols_fit)


Handcoded R-learner with Lasso last step

The previous exercise should have convinced you that these different transformations work. I prefer the second option, but this is a matter of taste. Instead of OLS we can similarly estimate a weighted Lasso with the pseudo-outcomes where we again save the estimated CATEs for later comparison:

# R-learner with Lasso
rlasso_hand = cv.glmnet(x,pseudo_rl,weights=weights_rl)
plot(rlasso_hand)

rlasso_hand = predict(rlasso_hand,newx = x, s = "lambda.min")


rlearner package

The R-learner can be more conveniently used via the rlearner package. It provides several options, but we focus on Lasso today, because the Boosting and Kernel options take quite some time to run. Note that this implementation uses now Lasso to estimate the nonlinear nuisance functions, which could deteriorate the performance. We store the predictions and will check below:

# Using the rlearner package
rlasso_fit = rlasso(x, w, y)
rlasso_est = predict(rlasso_fit, x)



DR-learner via causalDML package

The DR-learner uses the hopefully by now familiar pseudo-outcome of the AIPW ATE estimator and we will not handcode it again (see notebook SNB_GATE and replace the final OLS or Kernel regression step with the supervised ML method of your choice).

The causalDML package implements the DR-learner with the required cross-fitting procedure. It is a bit more complicated than what we discussed so far, but uses the same ideas we have learned. For details see the Appendix of Knaus (2020).

The default version of the dr_learner is implemented with an untuned honest Random Forest at all stages. This means it is well-suited for the nuisance parameters, but should have a harder time with the linear heterogeneous effects.

# DR-learner
dr_est = dr_learner(y,w,x)

Now it is time to compare the estimated CATEs of the estimators used so far to the, here known, true CATEs:

# Store and plot predictions
results1k = cbind(tau,r_ols_est,rlasso_hand,rlasso_est,dr_est$cates)
colnames(results1k) = c("True","RL OLS","RL Lasso hand","rlasso","DR RF")
pairs.panels(results1k,method = "pearson")

All estimators come quite close, but especially the DR-learner shows a lower correlation with the true CATEs. This is not unexpected because it uses Random Forest to approximate a linear CATE in its final step, while the other methods use OLS / Lasso that are expected to perform well with linear CATE functions.

Interestingly when checking the MSE, we see that the seemingly high performing rlasso implementation performs much worse in terms of MSE than the high correlation would suggest. It gets the sorting right, but not the levels as a look at the axes indicates.

# Compare MSE
data.frame(MSE = colMeans( (results1k[,-1]-c(tau))^2 ) ,
           Method = factor(colnames(results1k)[-1]) ) %>%
  ggplot(aes(x=Method,y=MSE)) + geom_point(size = 2) + 
  ggtitle(paste(toString(n),"observations")) + geom_hline(yintercept = 0)


With ensemble/SuperLearner

A computationally more expensive, but agnostic approach is to create an ensemble or SuperLearner. The idea is that we form predictions via several different supervised ML methods and use a weighted average of their predictions as final prediction. These weights are chosen in a data-driven way via cross-validation to figure out which predictors work best for the prediction model at hand (see Naimi and Balzer (2018) for a nice introduction with examples in R).

I am not aware of such an implementation for the R-learner. However, the dr_learner allows to use such an ensemble of methods. We specify it to use ensembles of several methods.

As the treatment is binary, we include the following methods to estimate the propensity score:

  • the mean (would be relevant if no selection into treatment)

  • self-tuned Random Forest

  • logistic Ridge regression

  • logistic Lasso regression

For outcome nuisances and the heterogeneous effect, we use

  • the mean (would be relevant in case of effect heterogeneity)

  • self-tuned Random Forest

  • OLS regression

  • Ridge regression

  • Lasso regression

## Create components of ensemble
# General methods
mean = create_method("mean")
forest =  create_method("forest_grf",args=list(tune.parameters = "all",honesty=F))

# Pscore specific components
ridge_bin = create_method("ridge",args=list(family = "binomial"))
lasso_bin = create_method("lasso",args=list(family = "binomial"))

# Outcome specific components
ols = create_method("ols")
ridge = create_method("ridge")
lasso = create_method("lasso")

# DR-learner with ensemble
dr_ens = dr_learner(y,w,x,ml_w=list(mean,forest,ridge_bin,lasso_bin),
                    ml_y = list(mean,forest,ols,ridge,lasso),
                    ml_tau = list(mean,forest,ols,ridge,lasso),quiet=T)

Let’s check how this performs.

# Add and plot predictions
label_method = c("RL OLS","RL Lasso hand","rlasso","DR RF","DR Ens")
results1k = cbind(tau,r_ols_est,rlasso_hand,rlasso_est,dr_est$cates,dr_ens$cates)
colnames(results1k) = c("True",label_method)
pairs.panels(results1k,method = "pearson")


# Compare MSE
data.frame(MSE = colMeans( (results1k[,-1]-c(tau))^2 ) ,
           Method = factor(label_method,levels=label_method) ) %>%
  ggplot(aes(x=Method,y=MSE)) + geom_point(size = 2) + 
  ggtitle(paste(toString(n),"observations")) + geom_hline(yintercept = 0)

It does not perform as well as the handcoded R-learner that uses suitable methods for each component. However, it improves already upon the only Lasso R-learner and the only Random Forest DR-learner in terms of MSE. I think this is impressive given that only 1000 observations are available to figure out which methods work best. However this should become better and better if we increase the sample size.

Thus we rerun the analysis with a draw of 4000 and of 16000 observations, which runs over two hours on my laptop.


4000 observations

# Increase sample size
n = 4000

# Draw sample
x = matrix(runif(n*p,-pi,pi),ncol=p)
tau = x %*% rho
w = rbinom(n,1,e(x[,1]))
y = m0(x[,1]) + w*tau + rnorm(n,0,1)

# Handcoded R-learner with OLS last stage
# C&P w/o comments from above
mhat = ehat = rep(NA,n)
index_s1 = sample(1:n,n/2)
x1 = x[index_s1,]
w1 = w[index_s1]
y1 = y[index_s1]
x2 = x[-index_s1,]
w2 = w[-index_s1]
y2 = y[-index_s1]
rf = regression_forest(x1,w1,tune.parameters = "all")
ehat[-index_s1] = predict(rf,newdata=x2)$predictions
rf = regression_forest(x1,y1,tune.parameters = "all")
mhat[-index_s1] = predict(rf,newdata=x2)$predictions
rf = regression_forest(x2,w2,tune.parameters = "all")
ehat[index_s1] = predict(rf,newdata=x1)$predictions
rf = regression_forest(x2,y2,tune.parameters = "all")
mhat[index_s1] = predict(rf,newdata=x1)$predictions
res_y = y-mhat
res_w = w-ehat
pseudo_rl = res_y / res_w
weights_rl = res_w^2
rols_fit = lm(pseudo_rl ~ x, weights=weights_rl)
r_ols_est = predict(rols_fit)

# Handcoded R-learner with Lasso last stage
rlasso_hand = cv.glmnet(x,pseudo_rl,weights=weights_rl)
rlasso_hand = predict(rlasso_hand,newx = x, s = "lambda.min")

# Using the rlearner package
rlasso_fit = rlasso(x, w, y)
rlasso_est = predict(rlasso_fit, x)

# DR-learner
dr_est = dr_learner(y,w,x)

# DR-learner with ensemble
dr_ens = dr_learner(y,w,x,ml_w=list(mean,forest,ridge_bin,lasso_bin),
                    ml_y = list(mean,forest,ols,ridge,lasso),
                    ml_tau = list(mean,forest,ols,ridge,lasso),quiet=T)
# Add and plot predictions
label_method = c("RL OLS","RL Lasso hand","rlasso","DR RF","DR Ens")
results4k = cbind(tau,r_ols_est,rlasso_hand,rlasso_est,dr_est$cates,dr_ens$cates)
colnames(results4k) = c("True",label_method)
pairs.panels(results4k,method = "pearson")

# Compare MSE
data.frame(MSE = colMeans( (results4k[,-1]-c(tau))^2 ) ,
           Method = factor(label_method,levels=label_method) ) %>%
  ggplot(aes(x=Method,y=MSE)) + geom_point(size = 2) + 
  ggtitle(paste(toString(n),"observations")) + geom_hline(yintercept = 0)


16000 observations

# Increase sample size
n = 16000

# Draw sample
x = matrix(runif(n*p,-pi,pi),ncol=p)
tau = x %*% rho
w = rbinom(n,1,e(x[,1]))
y = m0(x[,1]) + w*tau + rnorm(n,0,1)

# Handcoded R-learner with OLS last stage
# C&P w/o comments from above
mhat = ehat = rep(NA,n)
index_s1 = sample(1:n,n/2)
x1 = x[index_s1,]
w1 = w[index_s1]
y1 = y[index_s1]
x2 = x[-index_s1,]
w2 = w[-index_s1]
y2 = y[-index_s1]
rf = regression_forest(x1,w1,tune.parameters = "all")
ehat[-index_s1] = predict(rf,newdata=x2)$predictions
rf = regression_forest(x1,y1,tune.parameters = "all")
mhat[-index_s1] = predict(rf,newdata=x2)$predictions
rf = regression_forest(x2,w2,tune.parameters = "all")
ehat[index_s1] = predict(rf,newdata=x1)$predictions
rf = regression_forest(x2,y2,tune.parameters = "all")
mhat[index_s1] = predict(rf,newdata=x1)$predictions
res_y = y-mhat
res_w = w-ehat
pseudo_rl = res_y / res_w
weights_rl = res_w^2
rols_fit = lm(pseudo_rl ~ x, weights=weights_rl)
r_ols_est = predict(rols_fit)

# Handcoded R-learner with Lasso last stage
rlasso_hand = cv.glmnet(x,pseudo_rl,weights=weights_rl)
rlasso_hand = predict(rlasso_hand,newx = x, s = "lambda.min")

# Using the rlearner package
rlasso_fit = rlasso(x, w, y)
rlasso_est = predict(rlasso_fit, x)

# DR-learner
dr_est = dr_learner(y,w,x)

# DR-learner with ensemble
dr_ens = dr_learner(y,w,x,ml_w=list(mean,forest,ridge_bin,lasso_bin),
                    ml_y = list(mean,forest,ols,ridge,lasso),
                    ml_tau = list(mean,forest,ols,ridge,lasso),quiet=T)
# Add and plot predictions
label_method = c("RL OLS","RL Lasso hand","rlasso","DR RF","DR Ens")
results16k = cbind(tau,r_ols_est,rlasso_hand,rlasso_est,dr_est$cates,dr_ens$cates)
colnames(results16k) = c("True",label_method)
pairs.panels(results16k,method = "pearson")

# Compare MSE
data.frame(MSE = colMeans( (results16k[,-1]-c(tau))^2 ) ,
           Method = factor(label_method,levels=label_method) ) %>%
  ggplot(aes(x=Method,y=MSE)) + geom_point(size = 2) + 
  ggtitle(paste(toString(n),"observations")) + geom_hline(yintercept = 0)

With increasing sample size, the DR-learner with ensemble methods catches up with the methods tailored for the specific DGP. rlasso and the purely Random Forest based DR-learner continuously improve but are not competitive.

This highlights that using ensemble learners as an agnostic way to manage the prediction tasks in meta-learners works quite well. Especially in practice this can be a big advantage because it is a priori not clear which methods best approximate the nuisance parameters and heterogeneity. The big downside is of course that it takes much much longer to compute.



Take-away

  • Meta-learners are just combinations of different standard prediction problems \(\Rightarrow\) combine standard functions in a modular way

  • Using ensemble methods that figure out in a data-driven way which methods work for which prediction problem is an interesting option if we have time and no idea which single method works best in our setting



Suggestions to play with the toy model

Some suggestions:

  • Add Causal Forest to the comparison

  • Create a non-linear CATE and linear nuisance functions or

  • Change the treatment shares

LS0tDQp0aXRsZTogIkNhdXNhbCBNTDogTWV0YS1sZWFybmVyIg0Kc3VidGl0bGU6ICJTaW11bGF0aW9uIG5vdGVib29rIg0KYXV0aG9yOiAiTWljaGFlbCBLbmF1cyINCmRhdGU6ICJgciBmb3JtYXQoU3lzLnRpbWUoKSwgJyVtLyV5JylgIg0Kb3V0cHV0OiANCiAgaHRtbF9ub3RlYm9vazoNCiAgICB0b2M6IHRydWUNCiAgICB0b2NfZmxvYXQ6IHRydWUNCiAgICBjb2RlX2ZvbGRpbmc6IHNob3cNCi0tLQ0KDQoNCkdvYWxzOg0KDQotIEhhbmRjb2RlIFItbGVhcm5lcg0KDQotIElsbHVzdHJhdGUgdXNlIG9mIFItbGVhcm5lciBhbmQgRFItbGVhcm5lcg0KDQo8YnI+DQoNCg0KIyMgREdQDQoNCkNvbnNpZGVyIGEgREdQIHdpdGggbGluZWFyIGhldGVyb2dlbmVvdXMgdHJlYXRtZW50IGVmZmVjdHMsIGJ1dCBub25saW5lYXIgcHJvcGVuc2l0eSBzY29yZSBhbmQgb3V0Y29tZToNCg0KLSAkcD0xMCQgaW5kZXBlbmRlbnQgY292YXJpYXRlcyAkWF8xLC4uLixYX2ssLi4uLFhfezEwfSQgZHJhd24gZnJvbSBhIHVuaWZvcm0gZGlzdHJpYnV0aW9uOiAkWF9rIFxzaW0gdW5pZm9ybSgtXHBpLFxwaSkkDQoNCi0gVGhlIHRyZWF0bWVudCBtb2RlbCBpcyAkVyBcc2ltIEJlcm5vdWxsaShcdW5kZXJicmFjZXtcUGhpKHNpbihYXzEpKX1fe2UoWCl9KSQsIHdoZXJlICRcUGhpKFxjZG90KSQgaXMgdGhlIHN0YW5kYXJkIG5vcm1hbCBjdW11bGF0aXZlIGRlbnNpdHkgZnVuY3Rpb24NCg0KLSBUaGUgcG90ZW50aWFsIG91dGNvbWUgbW9kZWwgb2YgdGhlIGNvbnRyb2xzIGlzICRZKDApID0gXHVuZGVyYnJhY2V7c2luKFhfMSl9X3ttXzAoWCl9ICsgXHZhcmVwc2lsb24kLCB3aXRoICRcdmFyZXBzaWxvbiBcc2ltIE4oMCwxKSQNCg0KLSBUaGUgQ0FURSBmdW5jdGlvbiBpcyBhIGxpbmVhciBmdW5jdGlvbiBvZiB0aGUgZmlyc3QgdGhyZWUgY292YXJpYXRlcyAkXHRhdShYKSA9IFx1bmRlcmJyYWNlezAuM31fe1xyaG9fMX0gWF8xICsgXHVuZGVyYnJhY2V7MC4yfV97XHJob18yfSBYXzIgKyBcdW5kZXJicmFjZXswLjF9X3tccmhvXzN9IFhfMyQNCg0KLSBUaGUgcG90ZW50aWFsIG91dGNvbWUgbW9kZWwgb2YgdGhlIHRyZWF0ZWQgaXMgJFkoMSkgPSBtXzAoWCkgKyBcdGF1KFgpICsgXHZhcmVwc2lsb24kLCB3aXRoICRcdmFyZXBzaWxvbiBcc2ltIE4oMCwxKSQNCg0KV2UgZHJhdyBhIHNhbXBsZSBvZiAxMDAwIG9ic2VydmF0aW9ucyB0byBiZWdpbiB3aXRoOg0KDQpgYGB7ciwgd2FybmluZyA9IEYsIG1lc3NhZ2UgPSBGfQ0KaWYgKCFyZXF1aXJlKCJncmYiKSkgaW5zdGFsbC5wYWNrYWdlcygiZ3JmIiwgZGVwZW5kZW5jaWVzID0gVFJVRSk7IGxpYnJhcnkoZ3JmKQ0KaWYgKCFyZXF1aXJlKCJ0aWR5dmVyc2UiKSkgaW5zdGFsbC5wYWNrYWdlcygidGlkeXZlcnNlIiwgZGVwZW5kZW5jaWVzID0gVFJVRSk7IGxpYnJhcnkodGlkeXZlcnNlKQ0KaWYgKCFyZXF1aXJlKCJnbG1uZXQiKSkgaW5zdGFsbC5wYWNrYWdlcygiZ2xtbmV0IiwgZGVwZW5kZW5jaWVzID0gVFJVRSk7IGxpYnJhcnkoZ2xtbmV0KQ0KaWYgKCFyZXF1aXJlKCJwc3ljaCIpKSBpbnN0YWxsLnBhY2thZ2VzKCJwc3ljaCIsIGRlcGVuZGVuY2llcyA9IFRSVUUpOyBsaWJyYXJ5KHBzeWNoKQ0KaWYgKCFyZXF1aXJlKCJjYXVzYWxETUwiKSkgew0KICBpZiAoIXJlcXVpcmUoImRldnRvb2xzIikpIGluc3RhbGwucGFja2FnZXMoImRldnRvb2xzIiwgZGVwZW5kZW5jaWVzID0gVFJVRSk7IGxpYnJhcnkoZGV2dG9vbHMpDQogIGluc3RhbGxfZ2l0aHViKHJlcG89Ik1DS25hdXMvY2F1c2FsRE1MIikgDQp9OyBsaWJyYXJ5KGNhdXNhbERNTCkNCmlmICghcmVxdWlyZSgicmxlYXJuZXIiKSkgew0KICBpZiAoIXJlcXVpcmUoImRldnRvb2xzIikpIGluc3RhbGwucGFja2FnZXMoImRldnRvb2xzIiwgZGVwZW5kZW5jaWVzID0gVFJVRSk7IGxpYnJhcnkoZGV2dG9vbHMpDQogIGluc3RhbGxfZ2l0aHViKCJ4bmllL3JsZWFybmVyIikNCn07IGxpYnJhcnkocmxlYXJuZXIpDQoNCnNldC5zZWVkKDEyMzQpDQoNCiMgU2V0IHBhcmFtZXRlcnMNCm4gPSAxMDAwDQpwID0gMTANCg0KIyBDb3JyZWN0IHBhcmFtZXRlcnMNCnJobyA9IGMoMC4zLDAuMiwwLjEscmVwKDAscC0zKSkNCg0KIyBEcmF3IHNhbXBsZQ0KeCA9IG1hdHJpeChydW5pZihuKnAsLXBpLHBpKSxuY29sPXApDQplID0gZnVuY3Rpb24oeCl7cG5vcm0oc2luKHgpKX0NCm0wID0gZnVuY3Rpb24oeCl7c2luKHgpfQ0KdGF1ID0geCAlKiUgcmhvDQp3ID0gcmJpbm9tKG4sMSxlKHhbLDFdKSkNCnkgPSBtMCh4WywxXSkgKyB3KnRhdSArIHJub3JtKG4sMCwxKQ0KYGBgDQoNCjxicj4NCg0KIyMgSGFuZGNvZGVkIFItbGVhcm5lciB3aXRoIE9MUyBsYXN0IHN0ZXANCg0KRm9yIGlsbHVzdHJhdGlvbiBwdXJwb3NlcyB3ZSBoYW5kY29kZSBhbiBSLWxlYXJuZXIgd2l0aCBjcm9zcy1maXR0ZWQgbnVpc2FuY2UgcGFyYW1ldGVycyB2aWEgaG9uZXN0IFJhbmRvbSBGb3Jlc3QgYW5kIGhldGVyb2dlbmVpdHkgZXN0aW1hdGlvbiB2aWEgT0xTLiBUaGlzIHNob3VsZCB3b3JrIHdlbGwgYmVjYXVzZSB0aGUgbnVpc2FuY2UgcGFyYW1ldGVycyBhcmUgbm9ubGluZWFyIGFuZCBoZXRlcm9nZW5laXR5IGlzIGFjdHVhbGx5IGxpbmVhci4NCg0KRmlyc3QsIHdlIGdldCB0aGUgbnVpc2FuY2UgcGFyYW1ldGVycyAkZShYKT1FW1d8WF0kIGFuZCAkbShYKT1FW1l8WF0kIHZpYSBzZWxmLXR1bmVkIGhvbmVzdCBSYW5kb20gRm9yZXN0LiBXZSBoYW5kY29kZSAyLWZvbGQgY3Jvc3MtdmFsaWRhdGlvbiBpbiB0aGUgZmFtaWxpYXIgd2F5Og0KDQoNCmBgYHtyfQ0KIyBSLWxlYXJuZXIgd2l0aCBPTFMgbGFzdCBzdGFnZQ0KIyAyLWZvbGQgY3Jvc3MtZml0dGluZw0KbWhhdCA9IGVoYXQgPSByZXAoTkEsbikNCiMgRHJhdyByYW5kb20gaW5kaWNlcyBmb3Igc2FtcGxlIDENCmluZGV4X3MxID0gc2FtcGxlKDE6bixuLzIpDQojIENyZWF0ZSBTMQ0KeDEgPSB4W2luZGV4X3MxLF0NCncxID0gd1tpbmRleF9zMV0NCnkxID0geVtpbmRleF9zMV0NCiMgQ3JlYXRlIHNhbXBsZSAyIHdpdGggdGhvc2Ugbm90IGluIFMxDQp4MiA9IHhbLWluZGV4X3MxLF0NCncyID0gd1staW5kZXhfczFdDQp5MiA9IHlbLWluZGV4X3MxXQ0KIyBNb2RlbCBpbiBTMSwgcHJlZGljdCBpbiBTMg0KcmYgPSByZWdyZXNzaW9uX2ZvcmVzdCh4MSx3MSx0dW5lLnBhcmFtZXRlcnMgPSAiYWxsIikNCmVoYXRbLWluZGV4X3MxXSA9IHByZWRpY3QocmYsbmV3ZGF0YT14MikkcHJlZGljdGlvbnMNCnJmID0gcmVncmVzc2lvbl9mb3Jlc3QoeDEseTEsdHVuZS5wYXJhbWV0ZXJzID0gImFsbCIpDQptaGF0Wy1pbmRleF9zMV0gPSBwcmVkaWN0KHJmLG5ld2RhdGE9eDIpJHByZWRpY3Rpb25zDQojIE1vZGVsIGluIFMyLCBwcmVkaWN0IGluIFMxDQpyZiA9IHJlZ3Jlc3Npb25fZm9yZXN0KHgyLHcyLHR1bmUucGFyYW1ldGVycyA9ICJhbGwiKQ0KZWhhdFtpbmRleF9zMV0gPSBwcmVkaWN0KHJmLG5ld2RhdGE9eDEpJHByZWRpY3Rpb25zDQpyZiA9IHJlZ3Jlc3Npb25fZm9yZXN0KHgyLHkyLHR1bmUucGFyYW1ldGVycyA9ICJhbGwiKQ0KbWhhdFtpbmRleF9zMV0gPSBwcmVkaWN0KHJmLG5ld2RhdGE9eDEpJHByZWRpY3Rpb25zDQpgYGANCg0KV2UgaGF2ZSBub3cgdHdvIHdheXMgdG8gaW1wbGVtZW50IHRoZSBSLWxlYXJuZXINCg0KPGJyPg0KDQojIyMgMS4gTW9kaWZ5IHRoZSBjb3ZhcmlhdGVzDQoNClJlY2FsbCB0aGF0IHdlIGNhbiByZXdyaXRlIHRoZSBSLWxlYXJuZXIgYXMgbWluaW1pemluZyBhIGxlYXN0IHNxdWFyZXMgcHJvYmxlbSB3aXRoIG91dGNvbWUgcmVzaWR1YWwgYXMgcHNldWRvLW91dGNvbWUgYW5kIG1vZGlmaWVkIGNvdmFyaWF0ZXM6DQoNCiQkXGhhdHtcYmV0YX1ee3JsfSA9IGFyZ21pbl97XGJldGF9IFxzdW1fe2k9MX1eTiBcYmlnKFlfaSAtIFxoYXR7bX0oWF9pKSAtICBYX2leKiBcYmV0YSBcYmlnKV4yJCQNCg0Kd2hlcmUgJFhfaV4qID0gWF9pKFdfaSAtIFxoYXR7ZX0oWF9pKSkkIGFyZSB0aGUgbW9kaWZpZWQvcHNldWRvLWNvdmFyaWF0ZXMuDQoNCk5vdGUgdGhhdCAkWF9pJCBpbmNsdWRlcyB0aGUgY29uc3RhbnQgc3VjaCB0aGF0IHRoZSBmaXJzdCBjb2x1bW4gb2YgdGhlIG1vZGlmaWVkIGNvdmFyaWF0ZXMgZXF1YWxzIHRoZSB0cmVhdG1lbnQgcmVzaWR1YWxzIGFuZCB3ZSBydW4gYSByZWdyZXNzaW9uIHdpdGhvdXQgYSAicmVhbCIgY29uc3RhbnQgb2Ygb25lczoNCg0KYGBge3J9DQojIENyZWF0ZSByZXNpZHVhbHMNCnJlc195ID0geS1taGF0DQpyZXNfdyA9IHctZWhhdA0KDQojIE1vZGlmeSBjb3ZhcmlhdGVzIChtdWx0aXBseSBlYWNoIGNvbHVtbiBpbmNsdWRpbmcgY29uc3RhbnQgd2l0aCByZXNpZHVhbCkNCnhfd2MgPSBjYmluZChyZXAoMSxuKSx4KQ0KY29sbmFtZXMoeF93YykgPSBjKCJJbnRlcmNlcHQiLHBhc3RlMCgiWCIsMTpwKSkNCnhzdGFyID0geF93YyAqIHJlc193DQojIFJlZ3Jlc3Mgb3V0Y29tZSByZXNpZHVhbCBvbiBtb2RpZmllZCBjb3ZhcmlhdGVzDQpzdW1tYXJ5KGxtKHJlc195IH4gMCArIHhzdGFyKSkNCmBgYA0KDQpUaGUgZXN0aW1hdGVkIGNvZWZmaWNpZW50cyBhcmUgcmVsYXRpdmVseSBjbG9zZSB0byB0aGVpciB0cnVlIHZhbHVlcy4NCg0KPGJyPg0KDQojIyMgMi4gUHNldWRvLW91dGNvbWUgYW5kIHdlaWdodHMNCg0KVGhlIG1vcmUgZ2VuZXJpYyBhbHRlcm5hdGl2ZSBpcyB0byB1c2UgdGhlIHVubW9kaWZpZWQgY292YXJpYXRlcyBpbiBhIHdlaWdodGVkIHJlZ3Jlc3Npb24gd2l0aCBwc2V1ZG8gb3V0Y29tZXM6DQokJFxoYXR7XGJldGF9XntybH0gID0gYXJnbWluX3tcYmV0YX0gXHN1bV97aT0xfV5OIFx1bmRlcmJyYWNleyhXX2kgLSBcaGF0e2V9KFhfaSkpXjJ9X3tcdGV4dHt3ZWlnaHR9fSBcbGVmdChcdW5kZXJicmFjZXtcZnJhY3tZX2kgLSBcaGF0e219KFhfaSl9e1dfaSAtIFxoYXR7ZX0oWF9pKX19X3tcdGV4dHtwc2V1ZG8tb3V0Y29tZX19IC0gIFhfaSdcYmV0YSBccmlnaHQpXjIkJA0KDQpgYGB7cn0NCiMgQ3JlYXRlIHBzZXVkby1vdXRjb21lIChvdXRjb21lIHJlcyBkaXZpZGVkIGJ5IHRyZWF0bWVudCByZXMpDQpwc2V1ZG9fcmwgPSByZXNfeSAvIHJlc193DQoNCiMgQ3JlYXRlIHdlaWdodHMNCndlaWdodHNfcmwgPSByZXNfd14yDQoNCiMgV2VpZ2h0ZWQgcmVncmVzc2lvbiBvZiBwc2V1ZG8tb3V0Y29tZSBvbiBjb3ZhcmlhdGVzDQpyb2xzX2ZpdCA9IGxtKHBzZXVkb19ybCB+IHgsIHdlaWdodHM9d2VpZ2h0c19ybCkNCnN1bW1hcnkocm9sc19maXQpDQpyX29sc19lc3QgPSBwcmVkaWN0KHJvbHNfZml0KQ0KYGBgDQoNClRoaXMgcHJvZHVjZXMgdGhlIHNhbWUgcmVzdWx0cyBhcyB0aGUgbW9kaWZpZWQgY292YXJpYXRlIGFzIHRoZSBlcXVhbGl0eSBvZiBhbGwgY29lZmZpY2llbnRzIHNob3dzOg0KDQpgYGB7cn0NCiMgdGVzdCBpZiBhbGwgdmFsdWVzIGFyZSBlcXVhbA0KYWxsLmVxdWFsKGFzLm51bWVyaWMocm9sc19maXQkY29lZmZpY2llbnRzKSwgYXMubnVtZXJpYyhsbShyZXNfeSB+IDAgKyB4c3RhcikkY29lZmZpY2llbnRzKSkNCmBgYA0KDQpXZSBzdG9yZSB0aGUgZml0dGVkIHZhbHVlcyAkXGhhdHtcdGF1fSh4KSA9IHgnXGhhdHtcYmV0YX1ee3JsfSQgYXMgZXN0aW1hdGVzIG9mIHRoZSBDQVRFcyB0byBjb21wYXJlIHRoZW0gbGF0ZXIgd2l0aCB0aGUgdHJ1dGggYW5kIG90aGVyIG1ldGhvZHMuDQoNCmBgYHtyfQ0KIyBFc3RpbWF0ZSBDQVRFcw0Kcl9vbHNfZXN0ID0gcHJlZGljdChyb2xzX2ZpdCkNCmBgYA0KDQo8YnI+DQoNCiMjIEhhbmRjb2RlZCBSLWxlYXJuZXIgd2l0aCBMYXNzbyBsYXN0IHN0ZXANCg0KVGhlIHByZXZpb3VzIGV4ZXJjaXNlIHNob3VsZCBoYXZlIGNvbnZpbmNlZCB5b3UgdGhhdCB0aGVzZSBkaWZmZXJlbnQgdHJhbnNmb3JtYXRpb25zIHdvcmsuIEkgcHJlZmVyIHRoZSBzZWNvbmQgb3B0aW9uLCBidXQgdGhpcyBpcyBhIG1hdHRlciBvZiB0YXN0ZS4gSW5zdGVhZCBvZiBPTFMgd2UgY2FuIHNpbWlsYXJseSBlc3RpbWF0ZSBhIHdlaWdodGVkIExhc3NvIHdpdGggdGhlIHBzZXVkby1vdXRjb21lcyB3aGVyZSB3ZSBhZ2FpbiBzYXZlIHRoZSBlc3RpbWF0ZWQgQ0FURXMgZm9yIGxhdGVyIGNvbXBhcmlzb246DQoNCmBgYHtyfQ0KIyBSLWxlYXJuZXIgd2l0aCBMYXNzbw0Kcmxhc3NvX2hhbmQgPSBjdi5nbG1uZXQoeCxwc2V1ZG9fcmwsd2VpZ2h0cz13ZWlnaHRzX3JsKQ0KcGxvdChybGFzc29faGFuZCkNCnJsYXNzb19oYW5kID0gcHJlZGljdChybGFzc29faGFuZCxuZXd4ID0geCwgcyA9ICJsYW1iZGEubWluIikNCmBgYA0KPGJyPg0KDQojIyBgcmxlYXJuZXJgIHBhY2thZ2UNCg0KVGhlIFItbGVhcm5lciBjYW4gYmUgbW9yZSBjb252ZW5pZW50bHkgdXNlZCB2aWEgdGhlIGBybGVhcm5lcmAgcGFja2FnZS4gSXQgcHJvdmlkZXMgc2V2ZXJhbCBvcHRpb25zLCBidXQgd2UgZm9jdXMgb24gTGFzc28gdG9kYXksIGJlY2F1c2UgdGhlIEJvb3N0aW5nIGFuZCBLZXJuZWwgb3B0aW9ucyB0YWtlIHF1aXRlIHNvbWUgdGltZSB0byBydW4uIE5vdGUgdGhhdCB0aGlzIGltcGxlbWVudGF0aW9uIHVzZXMgbm93IExhc3NvIHRvIGVzdGltYXRlIHRoZSBub25saW5lYXIgbnVpc2FuY2UgZnVuY3Rpb25zLCB3aGljaCBjb3VsZCBkZXRlcmlvcmF0ZSB0aGUgcGVyZm9ybWFuY2UuIFdlIHN0b3JlIHRoZSBwcmVkaWN0aW9ucyBhbmQgd2lsbCBjaGVjayBiZWxvdzoNCg0KYGBge3IsIHdhcm5pbmcgPSBGfQ0KIyBVc2luZyB0aGUgcmxlYXJuZXIgcGFja2FnZQ0Kcmxhc3NvX2ZpdCA9IHJsYXNzbyh4LCB3LCB5KQ0Kcmxhc3NvX2VzdCA9IHByZWRpY3Qocmxhc3NvX2ZpdCwgeCkNCmBgYA0KDQo8YnI+DQo8YnI+DQoNCiMjIERSLWxlYXJuZXIgdmlhIGBjYXVzYWxETUxgIHBhY2thZ2UNCg0KVGhlIERSLWxlYXJuZXIgdXNlcyB0aGUgaG9wZWZ1bGx5IGJ5IG5vdyBmYW1pbGlhciBwc2V1ZG8tb3V0Y29tZSBvZiB0aGUgQUlQVyBBVEUgZXN0aW1hdG9yIGFuZCB3ZSB3aWxsIG5vdCBoYW5kY29kZSBpdCBhZ2FpbiAoc2VlIG5vdGVib29rIFtTTkJfR0FURV0oaHR0cHM6Ly9tY2tuYXVzLmdpdGh1Yi5pby9hc3NldHMvbm90ZWJvb2tzL1NOQi9TTkJfR0FURS5uYi5odG1sKSBhbmQgcmVwbGFjZSB0aGUgZmluYWwgT0xTIG9yIEtlcm5lbCByZWdyZXNzaW9uIHN0ZXAgd2l0aCB0aGUgc3VwZXJ2aXNlZCBNTCBtZXRob2Qgb2YgeW91ciBjaG9pY2UpLg0KDQpUaGUgYGNhdXNhbERNTGAgcGFja2FnZSBpbXBsZW1lbnRzIHRoZSBEUi1sZWFybmVyIHdpdGggdGhlIHJlcXVpcmVkIGNyb3NzLWZpdHRpbmcgcHJvY2VkdXJlLiBJdCBpcyBhIGJpdCBtb3JlIGNvbXBsaWNhdGVkIHRoYW4gd2hhdCB3ZSBkaXNjdXNzZWQgc28gZmFyLCBidXQgdXNlcyB0aGUgc2FtZSBpZGVhcyB3ZSBoYXZlIGxlYXJuZWQuIEZvciBkZXRhaWxzIHNlZSB0aGUgQXBwZW5kaXggb2YgW0tuYXVzICgyMDIwKV0oaHR0cHM6Ly9hcnhpdi5vcmcvYWJzLzIwMDMuMDMxOTEpLg0KDQpUaGUgZGVmYXVsdCB2ZXJzaW9uIG9mIHRoZSBgZHJfbGVhcm5lcmAgaXMgaW1wbGVtZW50ZWQgd2l0aCBhbiB1bnR1bmVkIGhvbmVzdCBSYW5kb20gRm9yZXN0IGF0IGFsbCBzdGFnZXMuIFRoaXMgbWVhbnMgaXQgaXMgd2VsbC1zdWl0ZWQgZm9yIHRoZSBudWlzYW5jZSBwYXJhbWV0ZXJzLCBidXQgc2hvdWxkIGhhdmUgYSBoYXJkZXIgdGltZSB3aXRoIHRoZSBsaW5lYXIgaGV0ZXJvZ2VuZW91cyBlZmZlY3RzLg0KDQoNCmBgYHtyfQ0KIyBEUi1sZWFybmVyDQpkcl9lc3QgPSBkcl9sZWFybmVyKHksdyx4KQ0KYGBgDQoNCg0KTm93IGl0IGlzIHRpbWUgdG8gY29tcGFyZSB0aGUgZXN0aW1hdGVkIENBVEVzIG9mIHRoZSBlc3RpbWF0b3JzIHVzZWQgc28gZmFyIHRvIHRoZSwgaGVyZSBrbm93biwgdHJ1ZSBDQVRFczoNCg0KYGBge3J9DQojIFN0b3JlIGFuZCBwbG90IHByZWRpY3Rpb25zDQpyZXN1bHRzMWsgPSBjYmluZCh0YXUscl9vbHNfZXN0LHJsYXNzb19oYW5kLHJsYXNzb19lc3QsZHJfZXN0JGNhdGVzKQ0KY29sbmFtZXMocmVzdWx0czFrKSA9IGMoIlRydWUiLCJSTCBPTFMiLCJSTCBMYXNzbyBoYW5kIiwicmxhc3NvIiwiRFIgUkYiKQ0KcGFpcnMucGFuZWxzKHJlc3VsdHMxayxtZXRob2QgPSAicGVhcnNvbiIpDQpgYGANCg0KQWxsIGVzdGltYXRvcnMgY29tZSBxdWl0ZSBjbG9zZSwgYnV0IGVzcGVjaWFsbHkgdGhlIERSLWxlYXJuZXIgc2hvd3MgYSBsb3dlciBjb3JyZWxhdGlvbiB3aXRoIHRoZSB0cnVlIENBVEVzLiBUaGlzIGlzIG5vdCB1bmV4cGVjdGVkIGJlY2F1c2UgaXQgdXNlcyBSYW5kb20gRm9yZXN0IHRvIGFwcHJveGltYXRlIGEgbGluZWFyIENBVEUgaW4gaXRzIGZpbmFsIHN0ZXAsIHdoaWxlIHRoZSBvdGhlciBtZXRob2RzIHVzZSBPTFMgLyBMYXNzbyB0aGF0IGFyZSBleHBlY3RlZCB0byBwZXJmb3JtIHdlbGwgd2l0aCBsaW5lYXIgQ0FURSBmdW5jdGlvbnMuDQoNCkludGVyZXN0aW5nbHkgd2hlbiBjaGVja2luZyB0aGUgTVNFLCB3ZSBzZWUgdGhhdCB0aGUgc2VlbWluZ2x5IGhpZ2ggcGVyZm9ybWluZyBgcmxhc3NvYCBpbXBsZW1lbnRhdGlvbiBwZXJmb3JtcyBtdWNoIHdvcnNlIGluIHRlcm1zIG9mIE1TRSB0aGFuIHRoZSBoaWdoIGNvcnJlbGF0aW9uIHdvdWxkIHN1Z2dlc3QuIEl0IGdldHMgdGhlIHNvcnRpbmcgcmlnaHQsIGJ1dCBub3QgdGhlIGxldmVscyBhcyBhIGxvb2sgYXQgdGhlIGF4ZXMgaW5kaWNhdGVzLg0KDQpgYGB7cn0NCiMgQ29tcGFyZSBNU0UNCmRhdGEuZnJhbWUoTVNFID0gY29sTWVhbnMoIChyZXN1bHRzMWtbLC0xXS1jKHRhdSkpXjIgKSAsDQogICAgICAgICAgIE1ldGhvZCA9IGZhY3Rvcihjb2xuYW1lcyhyZXN1bHRzMWspWy0xXSkgKSAlPiUNCiAgZ2dwbG90KGFlcyh4PU1ldGhvZCx5PU1TRSkpICsgZ2VvbV9wb2ludChzaXplID0gMikgKyANCiAgZ2d0aXRsZShwYXN0ZSh0b1N0cmluZyhuKSwib2JzZXJ2YXRpb25zIikpICsgZ2VvbV9obGluZSh5aW50ZXJjZXB0ID0gMCkNCmBgYA0KDQoNCjxicj4NCg0KIyMgV2l0aCBlbnNlbWJsZS9TdXBlckxlYXJuZXINCg0KQSBjb21wdXRhdGlvbmFsbHkgbW9yZSBleHBlbnNpdmUsIGJ1dCBhZ25vc3RpYyBhcHByb2FjaCBpcyB0byBjcmVhdGUgYW4gZW5zZW1ibGUgb3IgU3VwZXJMZWFybmVyLiBUaGUgaWRlYSBpcyB0aGF0IHdlIGZvcm0gcHJlZGljdGlvbnMgdmlhIHNldmVyYWwgZGlmZmVyZW50IHN1cGVydmlzZWQgTUwgbWV0aG9kcyBhbmQgdXNlIGEgd2VpZ2h0ZWQgYXZlcmFnZSBvZiB0aGVpciBwcmVkaWN0aW9ucyBhcyBmaW5hbCBwcmVkaWN0aW9uLiBUaGVzZSB3ZWlnaHRzIGFyZSBjaG9zZW4gaW4gYSBkYXRhLWRyaXZlbiB3YXkgdmlhIGNyb3NzLXZhbGlkYXRpb24gdG8gZmlndXJlIG91dCB3aGljaCBwcmVkaWN0b3JzIHdvcmsgYmVzdCBmb3IgdGhlIHByZWRpY3Rpb24gbW9kZWwgYXQgaGFuZCAoc2VlIFtOYWltaSBhbmQgQmFsemVyICgyMDE4KV0oaHR0cHM6Ly9saW5rLnNwcmluZ2VyLmNvbS9hcnRpY2xlLzEwLjEwMDcvczEwNjU0LTAxOC0wMzkwLXopIGZvciBhIG5pY2UgaW50cm9kdWN0aW9uIHdpdGggZXhhbXBsZXMgaW4gUikuDQoNCkkgYW0gbm90IGF3YXJlIG9mIHN1Y2ggYW4gaW1wbGVtZW50YXRpb24gZm9yIHRoZSBSLWxlYXJuZXIuIEhvd2V2ZXIsIHRoZSBgZHJfbGVhcm5lcmAgYWxsb3dzIHRvIHVzZSBzdWNoIGFuIGVuc2VtYmxlIG9mIG1ldGhvZHMuIFdlIHNwZWNpZnkgaXQgdG8gdXNlIGVuc2VtYmxlcyBvZiBzZXZlcmFsIG1ldGhvZHMuDQoNCkFzIHRoZSB0cmVhdG1lbnQgaXMgYmluYXJ5LCB3ZSBpbmNsdWRlIHRoZSBmb2xsb3dpbmcgbWV0aG9kcyB0byBlc3RpbWF0ZSB0aGUgcHJvcGVuc2l0eSBzY29yZToNCg0KLSB0aGUgbWVhbiAod291bGQgYmUgcmVsZXZhbnQgaWYgbm8gc2VsZWN0aW9uIGludG8gdHJlYXRtZW50KQ0KDQotIHNlbGYtdHVuZWQgUmFuZG9tIEZvcmVzdA0KDQotIGxvZ2lzdGljIFJpZGdlIHJlZ3Jlc3Npb24NCg0KLSBsb2dpc3RpYyBMYXNzbyByZWdyZXNzaW9uDQoNCkZvciBvdXRjb21lIG51aXNhbmNlcyBhbmQgdGhlIGhldGVyb2dlbmVvdXMgZWZmZWN0LCB3ZSB1c2UNCg0KLSB0aGUgbWVhbiAod291bGQgYmUgcmVsZXZhbnQgaW4gY2FzZSBvZiBlZmZlY3QgaGV0ZXJvZ2VuZWl0eSkNCg0KLSBzZWxmLXR1bmVkIFJhbmRvbSBGb3Jlc3QNCg0KLSBPTFMgcmVncmVzc2lvbg0KDQotIFJpZGdlIHJlZ3Jlc3Npb24NCg0KLSBMYXNzbyByZWdyZXNzaW9uDQoNCg0KDQpgYGB7cn0NCiMjIENyZWF0ZSBjb21wb25lbnRzIG9mIGVuc2VtYmxlDQojIEdlbmVyYWwgbWV0aG9kcw0KbWVhbiA9IGNyZWF0ZV9tZXRob2QoIm1lYW4iKQ0KZm9yZXN0ID0gIGNyZWF0ZV9tZXRob2QoImZvcmVzdF9ncmYiLGFyZ3M9bGlzdCh0dW5lLnBhcmFtZXRlcnMgPSAiYWxsIixob25lc3R5PUYpKQ0KDQojIFBzY29yZSBzcGVjaWZpYyBjb21wb25lbnRzDQpyaWRnZV9iaW4gPSBjcmVhdGVfbWV0aG9kKCJyaWRnZSIsYXJncz1saXN0KGZhbWlseSA9ICJiaW5vbWlhbCIpKQ0KbGFzc29fYmluID0gY3JlYXRlX21ldGhvZCgibGFzc28iLGFyZ3M9bGlzdChmYW1pbHkgPSAiYmlub21pYWwiKSkNCg0KIyBPdXRjb21lIHNwZWNpZmljIGNvbXBvbmVudHMNCm9scyA9IGNyZWF0ZV9tZXRob2QoIm9scyIpDQpyaWRnZSA9IGNyZWF0ZV9tZXRob2QoInJpZGdlIikNCmxhc3NvID0gY3JlYXRlX21ldGhvZCgibGFzc28iKQ0KDQojIERSLWxlYXJuZXIgd2l0aCBlbnNlbWJsZQ0KZHJfZW5zID0gZHJfbGVhcm5lcih5LHcseCxtbF93PWxpc3QobWVhbixmb3Jlc3QscmlkZ2VfYmluLGxhc3NvX2JpbiksDQogICAgICAgICAgICAgICAgICAgIG1sX3kgPSBsaXN0KG1lYW4sZm9yZXN0LG9scyxyaWRnZSxsYXNzbyksDQogICAgICAgICAgICAgICAgICAgIG1sX3RhdSA9IGxpc3QobWVhbixmb3Jlc3Qsb2xzLHJpZGdlLGxhc3NvKSxxdWlldD1UKQ0KYGBgDQoNCg0KTGV0J3MgY2hlY2sgaG93IHRoaXMgcGVyZm9ybXMuDQoNCg0KYGBge3J9DQojIEFkZCBhbmQgcGxvdCBwcmVkaWN0aW9ucw0KbGFiZWxfbWV0aG9kID0gYygiUkwgT0xTIiwiUkwgTGFzc28gaGFuZCIsInJsYXNzbyIsIkRSIFJGIiwiRFIgRW5zIikNCnJlc3VsdHMxayA9IGNiaW5kKHRhdSxyX29sc19lc3Qscmxhc3NvX2hhbmQscmxhc3NvX2VzdCxkcl9lc3QkY2F0ZXMsZHJfZW5zJGNhdGVzKQ0KY29sbmFtZXMocmVzdWx0czFrKSA9IGMoIlRydWUiLGxhYmVsX21ldGhvZCkNCnBhaXJzLnBhbmVscyhyZXN1bHRzMWssbWV0aG9kID0gInBlYXJzb24iKQ0KDQojIENvbXBhcmUgTVNFDQpkYXRhLmZyYW1lKE1TRSA9IGNvbE1lYW5zKCAocmVzdWx0czFrWywtMV0tYyh0YXUpKV4yICkgLA0KICAgICAgICAgICBNZXRob2QgPSBmYWN0b3IobGFiZWxfbWV0aG9kLGxldmVscz1sYWJlbF9tZXRob2QpICkgJT4lDQogIGdncGxvdChhZXMoeD1NZXRob2QseT1NU0UpKSArIGdlb21fcG9pbnQoc2l6ZSA9IDIpICsgDQogIGdndGl0bGUocGFzdGUodG9TdHJpbmcobiksIm9ic2VydmF0aW9ucyIpKSArIGdlb21faGxpbmUoeWludGVyY2VwdCA9IDApDQpgYGANCg0KSXQgZG9lcyBub3QgcGVyZm9ybSBhcyB3ZWxsIGFzIHRoZSBoYW5kY29kZWQgUi1sZWFybmVyIHRoYXQgdXNlcyBzdWl0YWJsZSBtZXRob2RzIGZvciBlYWNoIGNvbXBvbmVudC4gSG93ZXZlciwgaXQgaW1wcm92ZXMgYWxyZWFkeSB1cG9uIHRoZSBvbmx5IExhc3NvIFItbGVhcm5lciBhbmQgdGhlIG9ubHkgUmFuZG9tIEZvcmVzdCBEUi1sZWFybmVyIGluIHRlcm1zIG9mIE1TRS4gSSB0aGluayB0aGlzIGlzIGltcHJlc3NpdmUgZ2l2ZW4gdGhhdCBvbmx5IDEwMDAgb2JzZXJ2YXRpb25zIGFyZSBhdmFpbGFibGUgdG8gZmlndXJlIG91dCB3aGljaCBtZXRob2RzIHdvcmsgYmVzdC4gSG93ZXZlciB0aGlzIHNob3VsZCBiZWNvbWUgYmV0dGVyIGFuZCBiZXR0ZXIgaWYgd2UgaW5jcmVhc2UgdGhlIHNhbXBsZSBzaXplLg0KDQpUaHVzIHdlIHJlcnVuIHRoZSBhbmFseXNpcyB3aXRoIGEgZHJhdyBvZiA0MDAwIGFuZCBvZiAxNjAwMCBvYnNlcnZhdGlvbnMsIHdoaWNoIHJ1bnMgb3ZlciB0d28gaG91cnMgb24gbXkgbGFwdG9wLg0KDQo8YnI+DQoNCiMjIDQwMDAgb2JzZXJ2YXRpb25zDQoNCg0KYGBge3J9DQojIEluY3JlYXNlIHNhbXBsZSBzaXplDQpuID0gNDAwMA0KDQojIERyYXcgc2FtcGxlDQp4ID0gbWF0cml4KHJ1bmlmKG4qcCwtcGkscGkpLG5jb2w9cCkNCnRhdSA9IHggJSolIHJobw0KdyA9IHJiaW5vbShuLDEsZSh4WywxXSkpDQp5ID0gbTAoeFssMV0pICsgdyp0YXUgKyBybm9ybShuLDAsMSkNCg0KIyBIYW5kY29kZWQgUi1sZWFybmVyIHdpdGggT0xTIGxhc3Qgc3RhZ2UNCiMgQyZQIHcvbyBjb21tZW50cyBmcm9tIGFib3ZlDQptaGF0ID0gZWhhdCA9IHJlcChOQSxuKQ0KaW5kZXhfczEgPSBzYW1wbGUoMTpuLG4vMikNCngxID0geFtpbmRleF9zMSxdDQp3MSA9IHdbaW5kZXhfczFdDQp5MSA9IHlbaW5kZXhfczFdDQp4MiA9IHhbLWluZGV4X3MxLF0NCncyID0gd1staW5kZXhfczFdDQp5MiA9IHlbLWluZGV4X3MxXQ0KcmYgPSByZWdyZXNzaW9uX2ZvcmVzdCh4MSx3MSx0dW5lLnBhcmFtZXRlcnMgPSAiYWxsIikNCmVoYXRbLWluZGV4X3MxXSA9IHByZWRpY3QocmYsbmV3ZGF0YT14MikkcHJlZGljdGlvbnMNCnJmID0gcmVncmVzc2lvbl9mb3Jlc3QoeDEseTEsdHVuZS5wYXJhbWV0ZXJzID0gImFsbCIpDQptaGF0Wy1pbmRleF9zMV0gPSBwcmVkaWN0KHJmLG5ld2RhdGE9eDIpJHByZWRpY3Rpb25zDQpyZiA9IHJlZ3Jlc3Npb25fZm9yZXN0KHgyLHcyLHR1bmUucGFyYW1ldGVycyA9ICJhbGwiKQ0KZWhhdFtpbmRleF9zMV0gPSBwcmVkaWN0KHJmLG5ld2RhdGE9eDEpJHByZWRpY3Rpb25zDQpyZiA9IHJlZ3Jlc3Npb25fZm9yZXN0KHgyLHkyLHR1bmUucGFyYW1ldGVycyA9ICJhbGwiKQ0KbWhhdFtpbmRleF9zMV0gPSBwcmVkaWN0KHJmLG5ld2RhdGE9eDEpJHByZWRpY3Rpb25zDQpyZXNfeSA9IHktbWhhdA0KcmVzX3cgPSB3LWVoYXQNCnBzZXVkb19ybCA9IHJlc195IC8gcmVzX3cNCndlaWdodHNfcmwgPSByZXNfd14yDQpyb2xzX2ZpdCA9IGxtKHBzZXVkb19ybCB+IHgsIHdlaWdodHM9d2VpZ2h0c19ybCkNCnJfb2xzX2VzdCA9IHByZWRpY3Qocm9sc19maXQpDQoNCiMgSGFuZGNvZGVkIFItbGVhcm5lciB3aXRoIExhc3NvIGxhc3Qgc3RhZ2UNCnJsYXNzb19oYW5kID0gY3YuZ2xtbmV0KHgscHNldWRvX3JsLHdlaWdodHM9d2VpZ2h0c19ybCkNCnJsYXNzb19oYW5kID0gcHJlZGljdChybGFzc29faGFuZCxuZXd4ID0geCwgcyA9ICJsYW1iZGEubWluIikNCg0KIyBVc2luZyB0aGUgcmxlYXJuZXIgcGFja2FnZQ0Kcmxhc3NvX2ZpdCA9IHJsYXNzbyh4LCB3LCB5KQ0Kcmxhc3NvX2VzdCA9IHByZWRpY3Qocmxhc3NvX2ZpdCwgeCkNCg0KIyBEUi1sZWFybmVyDQpkcl9lc3QgPSBkcl9sZWFybmVyKHksdyx4KQ0KDQojIERSLWxlYXJuZXIgd2l0aCBlbnNlbWJsZQ0KZHJfZW5zID0gZHJfbGVhcm5lcih5LHcseCxtbF93PWxpc3QobWVhbixmb3Jlc3QscmlkZ2VfYmluLGxhc3NvX2JpbiksDQogICAgICAgICAgICAgICAgICAgIG1sX3kgPSBsaXN0KG1lYW4sZm9yZXN0LG9scyxyaWRnZSxsYXNzbyksDQogICAgICAgICAgICAgICAgICAgIG1sX3RhdSA9IGxpc3QobWVhbixmb3Jlc3Qsb2xzLHJpZGdlLGxhc3NvKSxxdWlldD1UKQ0KIyBBZGQgYW5kIHBsb3QgcHJlZGljdGlvbnMNCmxhYmVsX21ldGhvZCA9IGMoIlJMIE9MUyIsIlJMIExhc3NvIGhhbmQiLCJybGFzc28iLCJEUiBSRiIsIkRSIEVucyIpDQpyZXN1bHRzNGsgPSBjYmluZCh0YXUscl9vbHNfZXN0LHJsYXNzb19oYW5kLHJsYXNzb19lc3QsZHJfZXN0JGNhdGVzLGRyX2VucyRjYXRlcykNCmNvbG5hbWVzKHJlc3VsdHM0aykgPSBjKCJUcnVlIixsYWJlbF9tZXRob2QpDQpwYWlycy5wYW5lbHMocmVzdWx0czRrLG1ldGhvZCA9ICJwZWFyc29uIikNCmBgYA0KDQpgYGB7cn0NCiMgQ29tcGFyZSBNU0UNCmRhdGEuZnJhbWUoTVNFID0gY29sTWVhbnMoIChyZXN1bHRzNGtbLC0xXS1jKHRhdSkpXjIgKSAsDQogICAgICAgICAgIE1ldGhvZCA9IGZhY3RvcihsYWJlbF9tZXRob2QsbGV2ZWxzPWxhYmVsX21ldGhvZCkgKSAlPiUNCiAgZ2dwbG90KGFlcyh4PU1ldGhvZCx5PU1TRSkpICsgZ2VvbV9wb2ludChzaXplID0gMikgKyANCiAgZ2d0aXRsZShwYXN0ZSh0b1N0cmluZyhuKSwib2JzZXJ2YXRpb25zIikpICsgZ2VvbV9obGluZSh5aW50ZXJjZXB0ID0gMCkNCg0KYGBgDQoNCjxicj4NCg0KIyMgMTYwMDAgb2JzZXJ2YXRpb25zDQoNCmBgYHtyfQ0KIyBJbmNyZWFzZSBzYW1wbGUgc2l6ZQ0KbiA9IDE2MDAwDQoNCiMgRHJhdyBzYW1wbGUNCnggPSBtYXRyaXgocnVuaWYobipwLC1waSxwaSksbmNvbD1wKQ0KdGF1ID0geCAlKiUgcmhvDQp3ID0gcmJpbm9tKG4sMSxlKHhbLDFdKSkNCnkgPSBtMCh4WywxXSkgKyB3KnRhdSArIHJub3JtKG4sMCwxKQ0KDQojIEhhbmRjb2RlZCBSLWxlYXJuZXIgd2l0aCBPTFMgbGFzdCBzdGFnZQ0KIyBDJlAgdy9vIGNvbW1lbnRzIGZyb20gYWJvdmUNCm1oYXQgPSBlaGF0ID0gcmVwKE5BLG4pDQppbmRleF9zMSA9IHNhbXBsZSgxOm4sbi8yKQ0KeDEgPSB4W2luZGV4X3MxLF0NCncxID0gd1tpbmRleF9zMV0NCnkxID0geVtpbmRleF9zMV0NCngyID0geFstaW5kZXhfczEsXQ0KdzIgPSB3Wy1pbmRleF9zMV0NCnkyID0geVstaW5kZXhfczFdDQpyZiA9IHJlZ3Jlc3Npb25fZm9yZXN0KHgxLHcxLHR1bmUucGFyYW1ldGVycyA9ICJhbGwiKQ0KZWhhdFstaW5kZXhfczFdID0gcHJlZGljdChyZixuZXdkYXRhPXgyKSRwcmVkaWN0aW9ucw0KcmYgPSByZWdyZXNzaW9uX2ZvcmVzdCh4MSx5MSx0dW5lLnBhcmFtZXRlcnMgPSAiYWxsIikNCm1oYXRbLWluZGV4X3MxXSA9IHByZWRpY3QocmYsbmV3ZGF0YT14MikkcHJlZGljdGlvbnMNCnJmID0gcmVncmVzc2lvbl9mb3Jlc3QoeDIsdzIsdHVuZS5wYXJhbWV0ZXJzID0gImFsbCIpDQplaGF0W2luZGV4X3MxXSA9IHByZWRpY3QocmYsbmV3ZGF0YT14MSkkcHJlZGljdGlvbnMNCnJmID0gcmVncmVzc2lvbl9mb3Jlc3QoeDIseTIsdHVuZS5wYXJhbWV0ZXJzID0gImFsbCIpDQptaGF0W2luZGV4X3MxXSA9IHByZWRpY3QocmYsbmV3ZGF0YT14MSkkcHJlZGljdGlvbnMNCnJlc195ID0geS1taGF0DQpyZXNfdyA9IHctZWhhdA0KcHNldWRvX3JsID0gcmVzX3kgLyByZXNfdw0Kd2VpZ2h0c19ybCA9IHJlc193XjINCnJvbHNfZml0ID0gbG0ocHNldWRvX3JsIH4geCwgd2VpZ2h0cz13ZWlnaHRzX3JsKQ0Kcl9vbHNfZXN0ID0gcHJlZGljdChyb2xzX2ZpdCkNCg0KIyBIYW5kY29kZWQgUi1sZWFybmVyIHdpdGggTGFzc28gbGFzdCBzdGFnZQ0Kcmxhc3NvX2hhbmQgPSBjdi5nbG1uZXQoeCxwc2V1ZG9fcmwsd2VpZ2h0cz13ZWlnaHRzX3JsKQ0Kcmxhc3NvX2hhbmQgPSBwcmVkaWN0KHJsYXNzb19oYW5kLG5ld3ggPSB4LCBzID0gImxhbWJkYS5taW4iKQ0KDQojIFVzaW5nIHRoZSBybGVhcm5lciBwYWNrYWdlDQpybGFzc29fZml0ID0gcmxhc3NvKHgsIHcsIHkpDQpybGFzc29fZXN0ID0gcHJlZGljdChybGFzc29fZml0LCB4KQ0KDQojIERSLWxlYXJuZXINCmRyX2VzdCA9IGRyX2xlYXJuZXIoeSx3LHgpDQoNCiMgRFItbGVhcm5lciB3aXRoIGVuc2VtYmxlDQpkcl9lbnMgPSBkcl9sZWFybmVyKHksdyx4LG1sX3c9bGlzdChtZWFuLGZvcmVzdCxyaWRnZV9iaW4sbGFzc29fYmluKSwNCiAgICAgICAgICAgICAgICAgICAgbWxfeSA9IGxpc3QobWVhbixmb3Jlc3Qsb2xzLHJpZGdlLGxhc3NvKSwNCiAgICAgICAgICAgICAgICAgICAgbWxfdGF1ID0gbGlzdChtZWFuLGZvcmVzdCxvbHMscmlkZ2UsbGFzc28pLHF1aWV0PVQpDQojIEFkZCBhbmQgcGxvdCBwcmVkaWN0aW9ucw0KbGFiZWxfbWV0aG9kID0gYygiUkwgT0xTIiwiUkwgTGFzc28gaGFuZCIsInJsYXNzbyIsIkRSIFJGIiwiRFIgRW5zIikNCnJlc3VsdHMxNmsgPSBjYmluZCh0YXUscl9vbHNfZXN0LHJsYXNzb19oYW5kLHJsYXNzb19lc3QsZHJfZXN0JGNhdGVzLGRyX2VucyRjYXRlcykNCmNvbG5hbWVzKHJlc3VsdHMxNmspID0gYygiVHJ1ZSIsbGFiZWxfbWV0aG9kKQ0KcGFpcnMucGFuZWxzKHJlc3VsdHMxNmssbWV0aG9kID0gInBlYXJzb24iKQ0KYGBgDQoNCmBgYHtyfQ0KIyBDb21wYXJlIE1TRQ0KZGF0YS5mcmFtZShNU0UgPSBjb2xNZWFucyggKHJlc3VsdHMxNmtbLC0xXS1jKHRhdSkpXjIgKSAsDQogICAgICAgICAgIE1ldGhvZCA9IGZhY3RvcihsYWJlbF9tZXRob2QsbGV2ZWxzPWxhYmVsX21ldGhvZCkgKSAlPiUNCiAgZ2dwbG90KGFlcyh4PU1ldGhvZCx5PU1TRSkpICsgZ2VvbV9wb2ludChzaXplID0gMikgKyANCiAgZ2d0aXRsZShwYXN0ZSh0b1N0cmluZyhuKSwib2JzZXJ2YXRpb25zIikpICsgZ2VvbV9obGluZSh5aW50ZXJjZXB0ID0gMCkNCmBgYA0KDQpXaXRoIGluY3JlYXNpbmcgc2FtcGxlIHNpemUsIHRoZSBEUi1sZWFybmVyIHdpdGggZW5zZW1ibGUgbWV0aG9kcyBjYXRjaGVzIHVwIHdpdGggdGhlIG1ldGhvZHMgdGFpbG9yZWQgZm9yIHRoZSBzcGVjaWZpYyBER1AuIGBybGFzc29gIGFuZCB0aGUgcHVyZWx5IFJhbmRvbSBGb3Jlc3QgYmFzZWQgRFItbGVhcm5lciBjb250aW51b3VzbHkgaW1wcm92ZSBidXQgYXJlIG5vdCBjb21wZXRpdGl2ZS4NCg0KVGhpcyBoaWdobGlnaHRzIHRoYXQgdXNpbmcgZW5zZW1ibGUgbGVhcm5lcnMgYXMgYW4gYWdub3N0aWMgd2F5IHRvIG1hbmFnZSB0aGUgcHJlZGljdGlvbiB0YXNrcyBpbiBtZXRhLWxlYXJuZXJzIHdvcmtzIHF1aXRlIHdlbGwuIEVzcGVjaWFsbHkgaW4gcHJhY3RpY2UgdGhpcyBjYW4gYmUgYSBiaWcgYWR2YW50YWdlIGJlY2F1c2UgaXQgaXMgKmEgcHJpb3JpKiBub3QgY2xlYXIgd2hpY2ggbWV0aG9kcyBiZXN0IGFwcHJveGltYXRlIHRoZSBudWlzYW5jZSBwYXJhbWV0ZXJzIGFuZCBoZXRlcm9nZW5laXR5LiBUaGUgYmlnIGRvd25zaWRlIGlzIG9mIGNvdXJzZSB0aGF0IGl0IHRha2VzIG11Y2ggbXVjaCBsb25nZXIgdG8gY29tcHV0ZS4NCg0KPGJyPg0KPGJyPg0KDQoNCg0KIyMgVGFrZS1hd2F5DQogDQogLSBNZXRhLWxlYXJuZXJzIGFyZSBqdXN0IGNvbWJpbmF0aW9ucyBvZiBkaWZmZXJlbnQgc3RhbmRhcmQgcHJlZGljdGlvbiBwcm9ibGVtcyAkXFJpZ2h0YXJyb3ckIGNvbWJpbmUgc3RhbmRhcmQgZnVuY3Rpb25zIGluIGEgbW9kdWxhciB3YXkNCiANCiAtIFVzaW5nIGVuc2VtYmxlIG1ldGhvZHMgdGhhdCBmaWd1cmUgb3V0IGluIGEgZGF0YS1kcml2ZW4gd2F5IHdoaWNoIG1ldGhvZHMgd29yayBmb3Igd2hpY2ggcHJlZGljdGlvbiBwcm9ibGVtIGlzIGFuIGludGVyZXN0aW5nIG9wdGlvbiBpZiB3ZSBoYXZlIHRpbWUgYW5kIG5vIGlkZWEgd2hpY2ggc2luZ2xlIG1ldGhvZCB3b3JrcyBiZXN0IGluIG91ciBzZXR0aW5nDQogDQo8YnI+DQo8YnI+DQogDQogDQojIyBTdWdnZXN0aW9ucyB0byBwbGF5IHdpdGggdGhlIHRveSBtb2RlbA0KDQpTb21lIHN1Z2dlc3Rpb25zOg0KIA0KLSBBZGQgQ2F1c2FsIEZvcmVzdCB0byB0aGUgY29tcGFyaXNvbg0KDQotIENyZWF0ZSBhIG5vbi1saW5lYXIgQ0FURSBhbmQgbGluZWFyIG51aXNhbmNlIGZ1bmN0aW9ucyBvciANCg0KLSBDaGFuZ2UgdGhlIHRyZWF0bWVudCBzaGFyZXMNCg0KIA==