tag:blogger.com,1999:blog-77096370926840137252024-03-13T11:48:56.727-03:00Programando sem cafeínaTiago Albineli Mottahttp://www.blogger.com/profile/04371749360526831167noreply@blogger.comBlogger129125tag:blogger.com,1999:blog-7709637092684013725.post-48030081118176178012019-03-12T23:00:00.001-03:002019-03-12T23:00:50.418-03:00The non-experimentation result anti-patternMany of us have gone through the scenario of putting new software functionality into production and seeing tremendous improvement in business metrics. These results are often celebrated and attributed to this new achievement and this is used in presentations and corporate bonuses definitions.<br />
<br />
But can we attribute causal relation between the new functionality and the new business metrics?<br />
<br />
Before answering this question, however difficult it may be to abandon the emotive side of the functionality we have created, let us try to imagine some hypothesis that might have caused this improvement other than our change:<br />
<br />
<ul>
<li>An advertising campaign</li>
<li>An unexpected viral</li>
<li>The functionality created by another squad</li>
<li>A concurrent product crashes</li>
<li>It is a normal seasonal movement</li>
</ul>
<br />
These are just a few assumptions that could cause improvement in metrics. It is possible to mitigate each of them through some data analysis. But can you anticipate all other possible causal assumptions to be able to rule them out? Difficultly.<br />
<br />
Since you can not assign causality between the two events, the most you can say is that there is a correlation between them. And yet it may be a spurious correlation. Some examples of this kind of correlations <a href="http://www.tylervigen.com/spurious-correlations" target="_blank">can be seen on this site here</a>. It speaks, for example, about the strong correlation between the number of Nicolas Cage films per year and the number of drownings in the United States per year.<br />
<br />
So the answer to the initial question is no! It is not correct to attribute the improvement of the business metric just because of its new functionality. If you want to check whether a new feature causes a change in some metric, an <a href="https://en.wikipedia.org/wiki/A/B_testing" target="_blank">AB experiment</a> is required.<br />
<br />
In an AB experiment we have a variant (or alternative) called control (usually A) and a variant containing the modification to be validated (B). A random sample of users will receive the version of their product with variant A, and another random sample of users will see variant B. Everything in the two variants must be the same, except the modification made in variant B. With this you can control the environment and all those possible causes we reported above and more the others hypothesis we could not predict before.<br />
<br />
At the end of the experiment we ate going to be able to attribute the causality of the new functionality to the incremental metric because it was the only thing different in the whole environment. Taking into account of course all statistical variance due to the use of a sample, which can lead us to possible false positives in a minimal percentage of cases.<br />
<br />
AB experiments are the most accepted technique currently for you to determine the causality of a change in your product. They are not a new thing and are widely used as a part of the <a href="https://en.wikipedia.org/wiki/Scientific_method" target="_blank">scientific method</a>.<br />
<br />
However there is currently a large discussion in the statistical area regarding <a href="https://en.wikipedia.org/wiki/Causal_inference" target="_blank">causal inferences</a>, which would be models for inferring cause from one event to another without necessarily an experiment controlling all variables. This discussion became even stronger after the publication of <a href="http://bayes.cs.ucla.edu/WHY/" target="_blank">The Book of Why</a>.<br />
<br />
However, if you do not master the techniques of causal inference, it is best not to disclose cause and effect without being sure. I know it's very hard for us to leave our emotions aside for having participated in the development of that piece of software. But it's important to stay cool. And of course, always do AB experiments before putting something into production.<br />
<br />
This post is part of a series about <a href="https://www.slideshare.net/timotta/experimentation-anti-patterns" target="_blank">Experimentation Anti-Patterns</a>, a talk that I presented recently.<div class="blogger-post-footer"><script type="text/javascript"><!--
google_ad_client = "pub-8291320703218320";
/* 468x60, criado 06/03/09 */
google_ad_slot = "5590043178";
google_ad_width = 468;
google_ad_height = 60;
//-->
</script>
<script type="text/javascript"
src="http://pagead2.googlesyndication.com/pagead/show_ads.js">
</script></div>Tiago Albineli Mottahttp://www.blogger.com/profile/04371749360526831167noreply@blogger.com0tag:blogger.com,1999:blog-7709637092684013725.post-19593591997062124032019-01-13T21:51:00.000-03:002019-01-13T21:51:38.790-03:00Spark "first" function behavior on pandas dataframe Spark first function is used to choose a value after aggregating some dataset value.<br />
<br />
In python pandas we don't have this behavior as default after aggregating some dataframe, but we can do it easily we a few lines of code.<br />
<br />
Since I could not find this solution on Stack Overflow. But first, let's see what happen with a column with string type when we do not use a function like firtst:<br />
<br />
<pre>import pandas as pd
df = pd.DataFrame.from_records([
dict(k=1, i=10, t="a"),
dict(k=1, i=20, t="b"),
dict(k=1, i=20, t="c"),
])
df.groupby("k", as_index=False).sum()
</pre>
If we run the code bellow, we get this result:
<br />
<br />
<pre><b> k i</b>
0 1 50
</pre>
You can see that the column <b>t</b> was removed since you cannot sum it.<br />
<br />
Now, let's add the first aggregation function to this column:
<br />
<br />
<pre>first = lambda a: a.values[0] if len(a) > 0 else None
df.groupby("k", as_index=False).agg({'i': sum, 't': first})
</pre>
<pre></pre>
<pre></pre>
If we run the code bellow, we get this result:
<br />
<br />
<pre><b> k i t</b>
0 1 50 a
</pre>
<br />
Solved
<div class="blogger-post-footer"><script type="text/javascript"><!--
google_ad_client = "pub-8291320703218320";
/* 468x60, criado 06/03/09 */
google_ad_slot = "5590043178";
google_ad_width = 468;
google_ad_height = 60;
//-->
</script>
<script type="text/javascript"
src="http://pagead2.googlesyndication.com/pagead/show_ads.js">
</script></div>Tiago Albineli Mottahttp://www.blogger.com/profile/04371749360526831167noreply@blogger.com0tag:blogger.com,1999:blog-7709637092684013725.post-3723330432165713232017-09-03T12:35:00.000-03:002017-09-03T13:43:54.680-03:00"Learning to ranking" with xCLiMF python implementationAt Globo.com we try to optimize our personalized recommendation providing better content in a ranked list. We have a hybrid system that can automatically essemble collaborative filtering, content based and non personalized algorithms.<br />
<br />
But none of our implemented algorithms are intented to rank. Our best collaborative filtering algorithm, based on the paper Collaborative Filtering for Implicit Feedback Datasets, doesn't optimizes a ranking metric like MRR or NDCG.<br />
<br />
Even so we have good results of those algorithms choosing the best hyper parameters focusing in MRR, we thought we could get better results using algorithms focusing in optimizing a ranking metric: Our first hypothesis is <a href="https://github.com/timotta/xclimf" target="_blank">xCLiMF</a>.<br />
<br />
<a href="https://github.com/timotta/xclimf" target="_blank">xCLiMF</a> is a latent factor collaborative filtering variant wich optimizes a lower bound of the smoothed reciprocal rank of "relevant" items in ranked recommendation list. It is a evolution o CLiMF algorithm that can handle using multiple levels of relevance data instead of using only unary user preferences.<br />
<br />
<h4>
<b><i>Implementation references</i></b></h4>
<br />
Since <a href="https://github.com/timotta/xclimf" target="_blank">xCLiMF</a> is very similar to CLiMF, I implemented <a href="https://github.com/timotta/xclimf" target="_blank">xCLiMF</a> based on this python CLiMF repository ( <a href="https://github.com/gamboviol/climf" target="_blank">https://github.com/gamboviol/climf </a>). During the process I found an error and submitted a pull request ( <a href="https://github.com/gamboviol/climf/pull/2">https://github.com/gamboviol/climf/pull/2</a> ).<br />
<br />
I have also found a C++ xCLiMF implementantion by other brazilian guy ( <a href="https://github.com/gpoesia/xclimf">https://github.com/gpoesia/xclimf</a> ). But this solution have two problems: The first one is a bug reported here ( <a href="https://github.com/gpoesia/xclimf/issues/1">https://github.com/gpoesia/xclimf/issues/1</a> ) and the second one is related to performance, since it updates user and item vectors separately as two loops of other two chained loops.<br />
<br />
<h4>
My implementation</h4>
<br />
So my implementation also have some differences from original papers:<br />
<br />
<ul>
<li>In the paper they don't mention any kind of previously normalization, but it's always a good practice to do it for machine learning algorithms input data. So I normalized dividing all rating by the maximum rating in the dataset. Using original ratings lead to some math errors:</li>
<ul>
<li>Exponential function of large values causes math range error;</li>
<li>Exponential function of very large negative numbers get 0, and lead to some division by zero error; </li>
<li>Log function of negative numbers, caused by large exponentials, causes math domain error</li>
</ul>
</ul>
<ul>
<li>The paper experimentation setup uses N random ratings by user in training and in the cross validation data. Even so I have implemented this way, I have put an option to select 2N top ratings by user, randomly distributed in training and cross validation. For me, this last protocol reflected a more realistic kind of problem that <a href="https://github.com/timotta/xclimf" target="_blank">xCLiMF</a> is going to solve.</li>
<li>In the original paper they described the algorithm as two chained loops. Firstly I implemented as described in the paper. But them I updated the code to use a more vectorized solution, having a 54% improvement in the performance. I maintained the chained loop solution for comparison purpose ( <a href="https://github.com/timotta/xclimf/blob/master/tests/looping_update.py">https://github.com/timotta/xclimf/blob/master/tests/looping_update.py</a> )</li>
<li>For the objective function, the paper describes that it's not necessary to divide by the number of users, because it is only used for validating if the gradient ascending is still increasing after many interactions. But I made this division for interpretability purposes.</li>
</ul>
<br />
<br />
<h4>
Results</h4>
<br />
Using the experimentation protocol of N random ratings by user in training and in the cross validation, applied to the MovieLens 1M dataset, I got MRR variying from 0.08 to 0.09 at the 50 iteration using the hyper-parameters described in the paper.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://3.bp.blogspot.com/-FuaA1z0I1Uk/Wauq91GM03I/AAAAAAABEAA/hX-KcKjGcCkSIQn9MfZRiUAk-IRUojf4wCLcBGAs/s1600/xclimf.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="271" data-original-width="750" height="228" src="https://3.bp.blogspot.com/-FuaA1z0I1Uk/Wauq91GM03I/AAAAAAABEAA/hX-KcKjGcCkSIQn9MfZRiUAk-IRUojf4wCLcBGAs/s640/xclimf.png" width="640" /></a></div>
<br />
Using the top K experimentation protocol in the same dataset I got MRR varying from 0.2 to 0.26. I tried those two experimentation protocol with the Alternative Least Squares implemented at Spark and the maximum MRR I got was 0.01.<br />
<br />
The sequence of experiments, and how to run can be seen at <a href="https://github.com/timotta/xclimf">https://github.com/timotta/xclimf</a><br />
<br />
<h4>
Conclusions and future</h4>
<br />
Now we have a working <a href="https://github.com/timotta/xclimf" target="_blank">xCLiMF</a> implementation in python, but it is not feasible to train it with Globo.com dataset. Running it using MovieLens 1M dataset that have 6k users took 4 minutes. With MovieLens 20M dataset that have 130k users took more than one hour. Globo.com datasets are much bigger than that, if we count only the logged users, our dataset of users preferences on shows has more than 4 millions users, it would take more than 40 hours to run.<br />
<br />
That's why my next step is implementing this same algorithm on Spark. Since the updating user by user step is independent, it is possible to scale it running in parallel on our hadoop cluster.<br />
<br />
I appreciate any help, correction, suggestion or criticism<br />
<br />
<h4>
References</h4>
<br />
CLiMF: Learning to Maximize Reciprocal Rank with Collaborative Less-is-More Filtering<br />
Yue Shi, Martha Larson, Alexandros Karatzoglou, Nuria Oliver, Linas Baltrunas, Alan Hanjalic<br />
ACM RecSys 2012<br />
<br />
xCLiMF: Optimizing Expected Reciprocal Rank for Data with Multiple Levels of Relevance<br />
Yue Shia, Alexandros Karatzogloub, Linas Baltrunasb, Martha Larsona, Alan Hanjalica<br />
ACM RecSys 2013<br />
<br />
Collaborative Filtering for Implicit Feedback Datasets<br />
Yifan Hu, Yehuda Koren, Chris Volinsky<br />
IEEE, 2008<div class="blogger-post-footer"><script type="text/javascript"><!--
google_ad_client = "pub-8291320703218320";
/* 468x60, criado 06/03/09 */
google_ad_slot = "5590043178";
google_ad_width = 468;
google_ad_height = 60;
//-->
</script>
<script type="text/javascript"
src="http://pagead2.googlesyndication.com/pagead/show_ads.js">
</script></div>Tiago Albineli Mottahttp://www.blogger.com/profile/04371749360526831167noreply@blogger.com1tag:blogger.com,1999:blog-7709637092684013725.post-74020759303199406472015-11-28T19:23:00.001-03:002023-12-26T12:19:20.888-03:00Dados abertos do Web Democracia Nas minhas conversas com os participantes do <a href="http://recsys.acm.org/recsys15/" target="_blank">RecSys de 2015</a> em Viena foi possível notar o esgotamento deles em relação aos dados existentes para análise. Os acadêmicos longe de grandes empresas como Linkedin, Google, Facebook e Netflix, estavam sempre em busca de mais dados para tunar e implementar novos algoritmos. Em resumo, ninguém aguenta mais o MovieLens.<br />
<br />
Por essa razão resolvi disponibilizar todos os dados de avaliações de políticos no Web Democracia publicamente.<br />
<br />
Para garantir a privacidade dos usuários utilizei a total aleatorização dos IDs de forma a evitar qualquer engenharia reversa, deixando então as avaliações anônimas.<br /><div><br /></div><div>Os dados podem <a href="https://drive.google.com/file/d/0B5MO76SW3-RZMUdXTVRfUmdKdWs/view?usp=drive_link&resourcekey=0-4YXc-vYHLX3hiqkqJzZtUQ" rel="nofollow" target="_blank">ser baixados aqui</a>.<br />
<br />
Agora então os quase meio milhão de avaliações de políticos do Web Democracia é mais uma fonte de informação e de um domínio bem diferente que os tradicionais livros e filmes.</div><div class="blogger-post-footer"><script type="text/javascript"><!--
google_ad_client = "pub-8291320703218320";
/* 468x60, criado 06/03/09 */
google_ad_slot = "5590043178";
google_ad_width = 468;
google_ad_height = 60;
//-->
</script>
<script type="text/javascript"
src="http://pagead2.googlesyndication.com/pagead/show_ads.js">
</script></div>Tiago Albineli Mottahttp://www.blogger.com/profile/04371749360526831167noreply@blogger.com0tag:blogger.com,1999:blog-7709637092684013725.post-11143730445508509602013-10-14T23:29:00.003-03:002013-10-14T23:29:28.945-03:00Meu primeiro aplicativo AndroidPubliquei essa semana no Google Play meu primeiro aplicativo Android. O <a href="http://goo.gl/ALVu3H" target="_blank">9Gag Offline</a> trata-se de um aplicativo que permite que você baixe os gags do site <a href="http://goo.gl/ALVu3H" target="_blank">9Gag</a> para momentos em que não haja conexão com internet. Por exemplo se você for viajar, no avião você ficará isolado do mundo mas poderá rir bastante com os gags que estarão guardados no seu celular.<br />
<br />
Já fazia mais ou menos uns seis meses que estava estudando esporadicamente o sistema operacional da Google e fazendo diversos testes. A um mês atrás resolvi fechar de vez o aplicativo e colocá-lo ao público. Deu pra aprender bastante sobre o framework e reviver os problemas de sincronia e concorrência que haviam na época em que programava para desktop com Delphi, Kylix e Java Swing.<br />
<br />
Espero que gostem e se acharem qualquer problema ou tiverem alguma sugestão é só avisar. Para baixar o <a href="http://goo.gl/ALVu3H" target="_blank">9Gag Offline basta clicar aqui e ir ao Google Play</a>.<div class="blogger-post-footer"><script type="text/javascript"><!--
google_ad_client = "pub-8291320703218320";
/* 468x60, criado 06/03/09 */
google_ad_slot = "5590043178";
google_ad_width = 468;
google_ad_height = 60;
//-->
</script>
<script type="text/javascript"
src="http://pagead2.googlesyndication.com/pagead/show_ads.js">
</script></div>Tiago Albineli Mottahttp://www.blogger.com/profile/04371749360526831167noreply@blogger.com0tag:blogger.com,1999:blog-7709637092684013725.post-61416569788428868282013-07-25T15:22:00.000-03:002013-07-25T15:22:34.916-03:00Plugin de Fonética Portuguesa para ElasticSearchO ElasticSearch possui um <a target="_blank" href="https://github.com/elasticsearch/elasticsearch-analysis-phonetic">plugin fonético</a> que abrange diversas regras e padrões. No entanto nenhuma dessas regras é boa o bastante para as peculiaridades da lingua portuguesa.<br />
<br />
Pensando nisso e de posse de uma gramática portuguesa, forkei o repositório e criei um encoder com as regras de nosso estimado idioma lusitano. Como alguns meses depois ainda não obtive resposta alguma da equipe do plugin original, promovi então este encoder a um novo plugin: <a target="_blank" href="https://github.com/timotta/elasticsearch-fonetica-portuguesa">Portuguese Phonetic Plugin</a>.<br />
<br />
Para utilizá-lo você precisa clonar o projeto no github e instalá-lo:<br />
<br />
<pre>git clone https://github.com/timotta/elasticsearch-fonetica-portuguesa.git
cd elasticsearch-fonetica-portuguesa
./script/install.sh ~/Programas/elasticsearch-0.20.5</pre>
Depois é preciso configurar um analyser em <b>config/elasticsearch.yml</b>:
<pre>index :
analysis :
analyzer :
fonetico :
type : custom
tokenizer : standard
filter : [ standard, lowercase, foneticaportuguesa_filter, asciifolding ]
filter :
foneticaportuguesa_filter:
type : foneticaportuguesa
replace : false</pre>
Com tudo configurado, você pode criar um novo indice usando este analyser, como é mostrado abaixo:
<pre>$ curl localhost:9200/comfonetica -XPUT -d '
{
"mappings" :{
"document" : {
"analyzer" : "fonetico"
}
}
}'</pre>
Com o indice criado e configurado é possivel verificar as transformações de texto da seguinte forma:
<pre>$ curl -XGET 'localhost:9200/comfonetica/_analyze?analyzer=fonetico&pretty=true' -d chiclete
{
"tokens" : [ {
"token" : "XICLETE",
"start_offset" : 0,
"end_offset" : 8,
"type" : "<ALPHANUM>",
"position" : 1
}, {
"token" : "chiclete",
"start_offset" : 0,
"end_offset" : 8,
"type" : "<ALPHANUM>",
"position" : 1
} ]</pre>
Repare que a palavra se transformou em duas. Isso acontece porque a configuração <b>replace</b> do filter está como false.
<br/><br/>
Atualmente só testei o plugin com a versão 0.20.5 do <a href="http://www.elasticsearch.org/">elasticsearch</a>, se testarem em outras versões peço que <a href="https://github.com/timotta/elasticsearch-fonetica-portuguesa/issues">reporte no github</a>. Além disso, nem todas as regras fonéticas foram implementadas, então se você precisar de alguma que esteja faltando, colabore ou solicite lá também.
<div class="blogger-post-footer"><script type="text/javascript"><!--
google_ad_client = "pub-8291320703218320";
/* 468x60, criado 06/03/09 */
google_ad_slot = "5590043178";
google_ad_width = 468;
google_ad_height = 60;
//-->
</script>
<script type="text/javascript"
src="http://pagead2.googlesyndication.com/pagead/show_ads.js">
</script></div>Tiago Albineli Mottahttp://www.blogger.com/profile/04371749360526831167noreply@blogger.com0tag:blogger.com,1999:blog-7709637092684013725.post-87718092998190715012013-07-16T00:20:00.001-03:002013-07-16T00:20:38.339-03:00Complexidade Ciclomática com Pygenie recursivo<a href="https://github.com/mattvonrocketstein/pygenie" target="_blank">Pygenie</a> é uma biblioteca python bem simples para calcular a <a href="http://logbr.reflectivesurface.com/2008/11/12/conceitos-de-programacao-complexidade-ciclomatica/" target="_blank">complexidade ciclomática</a> de seus método. É tão simples, mas tão simples que para projetos mais complexos é preciso scriptar um pouco para que todos os diretórios sejam analisados.<br />
<br />
Para facilitar então a nossa vida <a href="http://musica.com.br/" target="_blank">aqui na empresa</a>, fiz uma contribuição (com pouca esperança de ser aceita pois o projeto está parado a dois anos) para que seja possível obter os resultado de forma recursiva. Com esta contribuição é possivel informar o parâmetro <b>-r</b> que analisará recursivamente todos os arquivos <b>.py</b> dentro de um diretório.<br />
<br />
<pre>$ pygenie complexity mylib -r</pre>
<br />
Se dentro de seu projeto houver algum arquivo ou diretório que você não deseja que seja analisado você pode utilizar um outro parâmetro adicionado, o -e, onde você informar um pattern para ser excluído. Por exemplo se houver um diretório de testes:<br />
<br />
<pre>$ pygenie complexity mylib -r -e tests</pre>
<br />
Caso você deseje utilizar desde já pode instalar o <a href="https://github.com/mattvonrocketstein/pygenie/pull/2" target="_blank">pygenie através do meu fork</a> e neste meio tempo tentar incentivar nosso amigo <a href="https://github.com/mattvonrocketstein">Matthew Von Rocketstein</a> a aceitar o <a href="https://github.com/mattvonrocketstein/pygenie/pull/2" target="_blank">pull request</a>.<div class="blogger-post-footer"><script type="text/javascript"><!--
google_ad_client = "pub-8291320703218320";
/* 468x60, criado 06/03/09 */
google_ad_slot = "5590043178";
google_ad_width = 468;
google_ad_height = 60;
//-->
</script>
<script type="text/javascript"
src="http://pagead2.googlesyndication.com/pagead/show_ads.js">
</script></div>Tiago Albineli Mottahttp://www.blogger.com/profile/04371749360526831167noreply@blogger.com0tag:blogger.com,1999:blog-7709637092684013725.post-89809347074536287242013-07-16T00:05:00.000-03:002013-07-16T00:05:20.117-03:00SlimIt, Head.js e django compressorAo configurar o <a href="http://django-compressor.readthedocs.org/en/latest/" target="_blank">django compressor</a> com o filtro do SlimIt, o arquivo do head.js comprimido pela templatetag <b>compress</b> era gerado com um caracter <b>ï</b> ao ínicio. Utilizando o SlimIt na mão eu obtinha o seguinte erro:<br />
<br />
<pre>$ slimit head.js
Illegal character '\xbb' at 1:1 after LexToken(ID,'\xef',1,0)
Illegal character '\xbf' at 1:2 after LexToken(ID,'\xef',1,0)
</pre>
<br />
Criei uma <a href="https://github.com/rspivak/slimit/issues/55" target="_blank">issue no repositório do SlimIt</a> acreditando ser um problema desta lib. E determinado a corrigir tal problema acabei descobrindo que a raíz deste era o <a href="http://headjs.com/">Head.js</a>. No arquivo fonte desta biblioteca javascript havia realmente um caracter estranho, que nenhum editor exibia, nem mesmo o Vi. No entanto, se abríssemos o arquivo via python com um simples:<br />
<br />
<pre>open("head.js").read()</pre>
<br />
Víamos claramente o caracter escondido. Espantosamente o único editor que testei e que exibiu o caracter estranho foi o editor online do github.<br />
<br />
<a href="https://github.com/headjs/headjs/pull/250">Forked, pull resqueted e merged</a>: Felizes para sempre.<div class="blogger-post-footer"><script type="text/javascript"><!--
google_ad_client = "pub-8291320703218320";
/* 468x60, criado 06/03/09 */
google_ad_slot = "5590043178";
google_ad_width = 468;
google_ad_height = 60;
//-->
</script>
<script type="text/javascript"
src="http://pagead2.googlesyndication.com/pagead/show_ads.js">
</script></div>Tiago Albineli Mottahttp://www.blogger.com/profile/04371749360526831167noreply@blogger.com0tag:blogger.com,1999:blog-7709637092684013725.post-9697708717632864972012-11-21T22:56:00.000-02:002012-11-22T11:50:01.187-02:00Liberando o GIL do python para paralelizar seu código com threadsNo Python o Global Interpreter Locker impede que duas threads executem ao mesmo tempo. Uma thread só é executada quando nenhuma outra estiver executando. A solução mais comum para aproveitar todos os cores de uma máquina em python é abandonar threads e utilizar vários processos. No entanto há uma maneira de aproveitar todos os cores com threads no python. Para isso vamos precisar criar uma extensão em C.<br />
<br />
Primeiro vamos criar uma extensão que não libere o GIL para podermos comparar e conferir a melhoria de performance depois. O pivô dessa extensão é a seguinte função:<br />
<br />
<span style="font-family: monospace;">static int reduce_com_gil(int max, int (*f)(int x, int y)) {</span><br />
<span style="font-family: monospace;"> int retorno = 0;</span><br />
<span style="font-family: monospace;"> int i;</span><br />
<span style="font-family: monospace;"> <max font="font" i="i"></max></span><span style="font-family: monospace;">for(i=0; i</span><max i="i" p="p" style="font-family: monospace;"> < max; i++){</max><br />
<span style="font-family: monospace;"> retorno = (*f)(retorno, i);</span><br />
<span style="font-family: monospace;"> }</span><br />
<span style="font-family: monospace;"> return retorno;</span><br />
<span style="font-family: monospace;">}</span><br />
<br />
Essa função faz algo similar ao que um reduce faria, mas indo de 0 ao valor indicado em <b>max</b>. Para cada iteração a função enviada como segundo parâmetro é executada. A idéia é causar um grande processamento para que meu core fique travado. O uso dela está descrito no código abaixo:<br />
<br />
<code>
static PyObject *antigil_calcular_com_gil(PyObject *self) {<br />
int valor = reduce_com_gil(100*1000, *antigil_calculos);<br />
char numero [5000];<br />
sprintf(numero, "%d", valor );<br />
return Py_BuildValue("s", numero);<br />
}</code><br />
<br />
Repare que eu passo para a função <b>reduce_com_gil</b> o ponteiro da função <b>antigil_calculos</b>. Essa outra função faz diversos cálculos a cada iteração do reduce. O nome antigil é o nome da extensão de exemplo. <a href="https://gist.github.com/fc4a84ade91284b8cf2d" target="_blank">O código completo da extensão pode ser visto aqui.</a><br />
<br />
Instalada a extensão, podemos testar a performance da lib com o seguinte código:<br />
<br />
<code>
import antigil<br />
antigil.calcular_com_gil()<br />
</code><br />
<br />
E o seguinte comando:<br />
<br />
<code>$ time python teste.py<br />
real 0m2.702s<br />
user 0m2.692s<br />
</code>
<br />
<br />
Mas o que a gente quer é saber como se comportam as threads. No caso o seguinte script abre 4 threads para aproveitar os 4 cores da minha máquina:<br />
<br />
<code>
import antigil<br />
from threading import Thread<br />
<br />
threads = []<br />
for i in xrange(4):<br />
t = Thread(target=antigil.calcular_com_gil)<br />
t.start()<br />
threads.append(t)<br />
<br />
for t in threads:<br />
t.join()<br />
</code>
<br />
<br />
No entando, acaba não aproveitando. Repare que ao executar 4 threads, o GIL age e impede que duas sejam executadas ao mesmo tempo. Dessa forma só um core da máquina é aproveitado. Isso pode ser observado pelo tempo total que é aproximadamente quatro vezes o tempo de execução de uma:<br />
<br />
<code>
$ time python teste.py<br />
real 0m10.910s<br />
user 0m10.881s<br />
</code>
<br />
<br />
Bom, vamos então à extensão não bloqueante. A chave do sucesso nesse caso são as macros <b>Py_BEGIN_ALLOW_THREADS</b> e <b>Py_END_ALLOW_THREADS</b> que respectivamente liberam o GIL e obtem o GIL de volta. Essas macros estão definidas em Python.h. O código da função reduce ficaria assim:<br />
<br />
<code>
static int reduce_sem_gil(int max, int (*f)(int x, int y)) {<br />
int retorno = 0;<br />
Py_BEGIN_ALLOW_THREADS<br />
int i;<br />
for(i=0; i<max i="i" p="p"> < max; i++){</max></code><br />
<code><max i="i" p="p"> retorno = (*f)(retorno, i);<br />
}<br />
Py_END_ALLOW_THREADS<br />
return retorno;<br />
}</max></code><br />
<br />
Pronto, basta criar a função <b>antigil_calcular_sem_gil</b> similar à <b>antigil_calcular_com_gil</b>, utilizando a função <b>reduce_sem_gil</b> e então podemos repetir o teste. No caso parametrizei o script de teste para que você possa escolher qual função deseja executar:<br />
<br />
<code>
import antigil<br />
from threading import Thread<br />
import sys<br />
<br />
if 'com-gil' in sys.argv:<br />
calcular = antigil.calcular_com_gil<br />
elif 'sem-gil' in sys.argv:<br />
calcular = antigil.calcular_sem_gil<br />
else:<br />
print "Informar com-gil ou sem-gil"<br />
exit(0)<br />
<br />
threads = []<br />
for i in xrange(4):<br />
t = Thread(target=calcular)<br />
t.start()<br />
threads.append(t)<br />
<br />
for t in threads:<br />
t.join()</code><br />
<br />
O resultado é bem animador e mostra bem que o código rodou em paralelo:<br />
<br />
<code>$ time python teste.py sem-gil<br />
real 0m3.414s<br />
user 0m13.413s</code><br />
<br />
Como o código utiliza apenas CPU, sem IO algum, ao aumentar para 8 threads, mesmo a versão que libera o GIL dobra de tempo pois só tenho disponível 4 cores na minha máquina. No entanto acredito que se fizer alguma operação de IO entre as macros <b>Py_BEGIN_ALLOW_THREADS</b> e <b>Py_END_ALLOW_THREADS</b> outras threads poderão ser executadas no caminho. Isso eu ainda preciso validar.<br />
<br />
Só é preciso ter muito cuidado pois código entre essas macros está em território perigoso. A alteração de váriaveis globais ou ponteiros que podem ser compartilhados entre outras threads podem causar erros inesperados a qualquer momento. Portanto, é importante utilizar somente váriaveis locais e dados copiados.<br />
<br />
<a href="https://gist.github.com/fc4a84ade91284b8cf2d" target="_blank">O código completo da extensão em C antigil pode ser visto aqui.</a><br />
<br />
<br /><div class="blogger-post-footer"><script type="text/javascript"><!--
google_ad_client = "pub-8291320703218320";
/* 468x60, criado 06/03/09 */
google_ad_slot = "5590043178";
google_ad_width = 468;
google_ad_height = 60;
//-->
</script>
<script type="text/javascript"
src="http://pagead2.googlesyndication.com/pagead/show_ads.js">
</script></div>Tiago Albineli Mottahttp://www.blogger.com/profile/04371749360526831167noreply@blogger.com2tag:blogger.com,1999:blog-7709637092684013725.post-43746208181799220932012-08-09T22:43:00.001-03:002012-08-09T22:47:21.161-03:00Ganhe convites de graça para a SEMCOMP<br />
Este ano o maior evento de tecnologia da Bahia, a <a href="http://infojr.com.br/semcomp/" target="_blank">Semana da Computação da UFBA</a>, será fantástica. A lista de palestrantes já confirmados é um dos pontos que chama a atenção, com presença de <a href="http://homepages.dcc.ufmg.br/~nivio/br/index.php" target="_blank">Nívio Ziviani</a>, <a href="https://github.com/tupy" target="_blank">Osvaldo Matos</a>, <a href="http://akitaonrails.com/" target="_blank">Fábio Akita</a>, Sérgio Cavalcante entre outros. Você pode ver a lista completa dos palestrantes aqui: <a href="http://infojr.com.br/semcomp/palestrantes">http://infojr.com.br/semcomp/palestrantes</a>.<br />
<br />
Os dois primeiros lotes de entradas ao evento se esgotaram logo nos primeiros dias de lançamento, e o terceiro pode estar em vias de acabar. Como a <a href="http://globo.com/">Globo.com</a> está patrocinando o evento, ela me deu cinco convites para que eu distribuisse por conta própria. Resolvi então fazer de uma maneira divertida.<br />
<br />
Para garantir o seu convite grátis para a SEMCOMP, basta fazer três coisas:<br />
<br />
<ol>
<li>Curtir o <a href="http://musica.com.br/" target="_blank">Música.com.br</a> (novo site de música da Globo.com) <a href="http://www.facebook.com/Musica.com.br" target="_blank">no Facebook</a>.</li>
<li>Criar uma playlist no <a href="http://musica.com.br/" target="_blank">Música.com.br</a>.</li>
<li>Enviar a url dela para <a href="mailto:timotta@gmail.com">timotta@gmail.com</a> indicando em quais quesitos ela se encaixa, lembrando de colocar no assunto do e-mail: Playlist SEMCOMP.</li>
</ol>
<br />
O criador da melhor playlist em cada um dos seguintes quesitos abaixo leva um convite para o evento.<br />
<br />
<ul>
<li>Melhor playlist para programar em par</li>
<li>Melhor playlist para resolver bugs</li>
<li>Melhor playlist para configurar servidor</li>
<li>Melhor playlist para ajeitar layout web</li>
<li>Melhor playlist para programar sozinho</li>
</ul>
<br />
Você pode criar quantas playlists quiser. O próprio time de desenvolvimento do Música.com.br escolherá, em um método científico de experimentação, ou seja programando usando as playlists enviadas. Os vencedores serão anunciados apartir de 10 de Setembro de 2012 na <a href="http://www.facebook.com/semanacomp" target="_blank">página do facebook da SEMCOMP</a>.<br />
<br />
Mas veja que são apenas os convites. Não está incluído aí passagem ou hospedagem. Ou seja, caso você more em outro estado ou cidade, você irá arcar com esses custos. Será oferecido apenas o convite.<br />
<br />
Para servir de inspiração, segue algumas das playlists que eu criei:<br />
<br />
<ul>
<li><a href="http://musica.com.br/perfil/timotta/playlists/164.html" target="_blank">Trilha sonora de festa brega</a></li>
<li><a href="http://musica.com.br/perfil/timotta/playlists/165.html" target="_blank">Zé druguinha songs</a></li>
<li><a href="http://musica.com.br/perfil/timotta/playlists/539.html" target="_blank">Melhores covers do The Kooks</a></li>
</ul>
<br />
Que a diversão comece!<div class="blogger-post-footer"><script type="text/javascript"><!--
google_ad_client = "pub-8291320703218320";
/* 468x60, criado 06/03/09 */
google_ad_slot = "5590043178";
google_ad_width = 468;
google_ad_height = 60;
//-->
</script>
<script type="text/javascript"
src="http://pagead2.googlesyndication.com/pagead/show_ads.js">
</script></div>Tiago Albineli Mottahttp://www.blogger.com/profile/04371749360526831167noreply@blogger.com0tag:blogger.com,1999:blog-7709637092684013725.post-47134266893285038052012-07-22T18:18:00.000-03:002012-07-22T23:06:13.070-03:00Divagações sobre GIL, threads e IO em python e ruby<br />
Uma coisa que eu sempre me confundi sobre ruby e python é a questão do Global Interpreter Locker (GIL). Na verdade, a dúvida maior é se operações de IO realmente bloqueiam os processos, impedindo a execução de outras threads.<br />
<br />
Recentemente li um pouco mais sobre a versão 1.9 do Ruby e como ela passou a utilizar threads do sistema operacional, ao contrário das chamadas <a href="http://www.kumpera.net/blog/index.php/2007/04/12/green-threads-ideia-ruim-ou-implementacoes-pessimas/" target="_blank">Green Threads</a> da versão 1.8. No entanto, o GIL do ruby continua impedindo que duas threads executem ao mesmo tempo. A menos que uma esteja parada executando alguma operação de IO não bloqueante.<br />
<br />
No caso do Python, o pouco de informação que tenho me leva a crer que a linguagem utiliza <a href="http://www.kumpera.net/blog/index.php/2007/04/12/green-threads-ideia-ruim-ou-implementacoes-pessimas/" target="_blank">Green Threads</a>. Fica então a minha dúvida se mesmo assim é possivel que o processo execute uma outra thread enquanto aguarda um retorno de IO.<br />
<br />
Para começar fiz um pequeno script python para simular uma query pesada do MySql sendo executada 4 vezes. Se o script demorar por volta de dois segundo significa que durante uma operação de IO, o python prosseguiu executando as outras threads:<br />
<br />
<code>
import _mysql<br />
from threading import Thread<br />
<br />
def executa():<br />
db = _mysql.connect(host="localhost",</code><br />
<code> user="root",<br />
passwd="",</code><br />
<code> db="teste")<br />
db.query("select sleep(2)")<br />
r=db.use_result()<br />
r.fetch_row()<br />
<br />
threads = []<br />
for i in range(4): <br />
t = Thread(target=executa, args=())<br />
t.start()<br />
threads.append(t)<br />
<br />
for t in threads:<br />
t.join()<br />
<br />
</code>
O resultado da execução pode ser visto abaixo, mostrando que o IO não bloqueou o programa:<br />
<br />
<code>
> time python teste.py<br />
real<span class="Apple-tab-span" style="white-space: pre;"> </span>0m2.030s<br />
user<span class="Apple-tab-span" style="white-space: pre;"> </span>0m0.016s<br />
sys<span class="Apple-tab-span" style="white-space: pre;"> </span>0m0.012s<br />
</code>
<br />
Executei o mesmo teste com ruby, com o script similar abaixo:<br />
<br />
<code>
require 'mysql2'<br />
<br />
threads = []<br />
4.times do |i|<br />
thread = Thread.new do<br />
my = Mysql2::Client.new(host: "127.0.0.1", </code><br />
<code> username: "root", </code><br />
<code> database: "teste")</code><br />
<code> my.query("select sleep(2)").collect{|i|i}<br />
end<br />
thread.run<br />
threads << thread<br />
end<br />
<br />
threads.each do |thread| <br />
thread.join<br />
end<br />
</code>
<br />
E o resultado também foi satisfatório:<br />
<br />
<code>
> time ruby teste.rb<br />
real<span class="Apple-tab-span" style="white-space: pre;"> </span>0m2.178s<br />
user<span class="Apple-tab-span" style="white-space: pre;"> </span>0m0.076s<br />
sys<span class="Apple-tab-span" style="white-space: pre;"> </span>0m0.008s<br />
</code>
<br />
Ou seja, tanto python como ruby estão lidando bem com execução paralela. Mesmo se não utilizar todos os cores disponiveis, no mínimo o IO para o MySql não está bloqueando.<br />
<br />
Com esse bom resultado, resolvi então subir um pouco mais de nível e verificar se colocando o Django na equação poderíamos aproveitar esse bom desempenho. Criei essa pequena view para simular uma query lenta, assim como nos scripts acima, e iniciei o Django (versão 1.3) com o gunicorn.<br />
<br />
<code>
from django.db import connection<br />
def debug(request):<br />
cursor = connection.cursor()<br />
cursor.execute("select sleep(2)")<br />
return HttpResponse('ok')<br />
</code>
<br />
E o resultado, como pode ser visto abaixo, foi ruim:<br />
<br />
<code>
> ab -n 4 -c 4 http://127.0.0.1:8000/debug/<br />
Time taken for tests: 8.161 seconds<br />
</code>
<br />
O mesmo teste executado para a dupla ruby on rails tem resultado parecido:<br />
<br />
<code>
> ab -n 4 -c 4 http://localhost:3000/politicos/<br />
Time taken for tests: 8.043 seconds<br />
</code>
<br />
Conclusão:<br />
<br />
Embora python e ruby permitam IO não bloqueante, os frameworks Django e Rails ainda são bloqueantes. Um dos motivos daqueles memes de "Rails não escala" e "Django não escala". Felizmente sempre há alternativas.<br />
<br />
É possivel escalar via processos como explicado em <a href="http://programandosemcafeina.blogspot.com.br/2010/09/solucionando-io-bloqueante-do-mysql-no.html" target="_blank">Solucionando IO bloqueante do mysql</a> para Rails, e utilizando vários workers do gunicorn para Django. Caso seja necessário uma escalabilidade maior ainda, o ideal então é partir para soluções como Event Machine do ruby, GEvent para Python, ou até mesmo NodeJs. Combinando essas soluções com múltiplos processos.<br />
<div>
<br /></div><div class="blogger-post-footer"><script type="text/javascript"><!--
google_ad_client = "pub-8291320703218320";
/* 468x60, criado 06/03/09 */
google_ad_slot = "5590043178";
google_ad_width = 468;
google_ad_height = 60;
//-->
</script>
<script type="text/javascript"
src="http://pagead2.googlesyndication.com/pagead/show_ads.js">
</script></div>Tiago Albineli Mottahttp://www.blogger.com/profile/04371749360526831167noreply@blogger.com1tag:blogger.com,1999:blog-7709637092684013725.post-400448347891700092012-07-09T01:55:00.001-03:002012-07-09T02:20:28.084-03:00Estratégias de persitência do RedisNestes ultimos meses eu ministrei algumas palestras sobre o uso de <a href="http://www.slideshare.net/timotta/redis-na-prtica" target="_blank">redis na prática</a>, usando como exemplo algumas funcionalidades do <a href="http://musica.com.br/" target="_blank">Musica.com.br</a>. Uma das dúvidas mais constantes nesses eventos é a questão das opções de persistência deste banco. Confesso que não havia estudado o suficiente sobre o assunto. Eis que com o crescimento do site, surgiu a necessidade de uma estratégia melhor sobre a manutenção desses dados. Então aproveitei minhas horas em aeroportos para realizar alguns testes e responder algumas dúvidas.<br />
<br />
Pra começar o <a href="http://redis.io/" target="_blank">Redis</a> possui três formas de persitência de dados: não persistir, dump no disco e append only. O dump no disco, basicamente consiste em gravar uma cópia da memória em disco de tempos em tempos. O append only grava todos os comandos em um log, de forma que para recuperar em caso de restarte ele refaz todo o caminho percorrido.<br />
<br />
Vamos então às perguntas que eu me fazia, e as respostas que obtive com meus testes:<br />
<br />
1) É possível converter o rdb (dump) para um aof (appendonly)?<br />
<br />
Sim para isso é preciso executar o comando <a href="http://redis.io/commands/bgrewriteaof" target="_blank">BGREWRITEAOF</a> estopar o redis server e estartar de novo com a nova configuração habilitada para appendonly.<br />
<br />
2) O tempo para gravar e carregar o rdb (dump) é grande?<br />
<br />
Para os padrões do Redis gravar é sim um tempo grande. Mas talvez não seja algo que possa atrapalhar. Depende muito da quantidade de dados que sua base terá. Fiz um teste adicionando três vezes 1 milhão de registros e comparando o tempo que demorava para gerar o dump em disco. Essa tabela pode servir de guia para saber quando é o momento de colocar o seu redis em uma máquina separada, e quando é hora de trocar de estratégia de gravação.<br />
<br />
<table border="1px">
<tbody>
<tr><td>Registros</td><td>Memória</td><td>Disco</td><td>Tempo para salvar</td><td>Gravação de Memória/segundo</td></tr>
<tr><td>1.000.000</td><td>99.94M</td><td>24M</td><td>0.56s</td><td>178.46 M/s</td></tr>
<tr><td>2.000.000</td><td>191.50M</td><td>42M</td><td>0.93s</td><td>205.37 M/s</td></tr>
<tr><td>3.000.000</td><td>283.03M</td><td>62M</td><td>1.32s</td><td>214.41 M/s</td></tr>
</tbody></table>
<br />
No entanto, o load do dump com 3 milhões de registros foi irrisório.<br />
<br />
3) O tempo para carregar o aof (appendonly) é grande?<br />
<br />
Sim, muito grande. Fiz um teste similar ao do dump, com 3 milhões de registros colocando 313.57M em memória e gerando um arquivo aof de 192M. Ao restartar o servidor ele demorou 4 segundos para carregar o arquivo. Um resultado que achei péssimo. No entanto, se pensarmos que restartar servidor é algo que não faremos com tanta constância, pode não ser algo tão ruim. Só deve-se ficar em mente que manutenções desse tipo em uma base grande devem ser em horários com pouco ou nenhum uso do seu servidor master.<br />
<br />
4) É possivel compactar o aof (appendonly) com <a href="http://redis.io/commands/bgrewriteaof" target="_blank">BGREWRITEAOF</a>?<br />
<br />
Um dos problemas do aof é que se um registro for incluido e removido, as duas instruções serão gravadas, de forma que não há uma razão clara entre memória do servidor e tamanho do arquivo em disco gerado. Minha dúvida era se eu poderia limpar o aof reescrevendo somente aquilo que estava na memória. E sim, isso é possível com o comando <a href="http://redis.io/commands/bgrewriteaof" target="_blank">BGREWRITEAOF</a>.<br />
<br />
5) Os comandos ficam mais lentos com aof (appendonly) ligado?<br />
<br />
Sim. Fiz um teste com concorrência. Três processos inserindo 1 milão de itens em listas diferentes. Com rdb (dump) habilitado a média de tempo foi de 0.33s. Com aof (appendonly) habilitado e rdb (dump) desabilitado a média de tempo foi de 0.43s. Essa proporção de 30% mais demorado para aof foi constante nos três testes seguintes que fiz como tira-teima.<br />
<br />
6) Se o redis slave ficar down por um tempo ele recebe os dados do master?<br />
<br />
Sim, fiz uma série de testes e percebi que o redis slave ignora os arquivos salvos por ele mesmo. Sempre que se inicia ele recebe todos os dados novamente do master, seja ele configurado como rdb ou como aof. Portanto, se estiver utilizando master slave, uma boa é configurar o seu slave para não gravar nada.<br />
<br />
7) Gravar o rdb (dump) afeta a performance na resposta a outros comandos?<br />
<br />
Sim, fiz um teste gravando três vezes uma memória de 2GB enquanto estava inserindo 1 milhão de novos registros. A insersão deles demorou 25% mais que inserir a mesma quantidade de registros sem salvar nenhuma vez o rdb. Só para se ter uma idéia, o tempo de gravação deste dump foi de 9 segundos e o tempo de load foi de 4 segundos.<br />
<br />
Conclusão:<br />
<br />
- Manter seus dados com rdb deixa o redis mais rápido, mas pode exigir muito de IO nos intervalos de persistência. Se a memória estiver muito grande, vale a pena colocar o servidor master em uma máquina dedicada para que não atrapalhe outros serviços.<br />
<br />
- Manter seus dados com aof deixa o redis mais lento, mas garante que nenhuma transação será perdida. É preciso ficar atento para o arquivo gerado e periodicamente compactar ele com <a href="http://redis.io/commands/bgrewriteaof" target="_blank">BGREWRITEAOF</a>. Também tomar cuidado nos restarts que podem demorar.<br />
<br /><div class="blogger-post-footer"><script type="text/javascript"><!--
google_ad_client = "pub-8291320703218320";
/* 468x60, criado 06/03/09 */
google_ad_slot = "5590043178";
google_ad_width = 468;
google_ad_height = 60;
//-->
</script>
<script type="text/javascript"
src="http://pagead2.googlesyndication.com/pagead/show_ads.js">
</script></div>Tiago Albineli Mottahttp://www.blogger.com/profile/04371749360526831167noreply@blogger.com1tag:blogger.com,1999:blog-7709637092684013725.post-88108852036556083762012-05-02T01:44:00.001-03:002012-05-02T01:50:48.909-03:00Bate-papo sobre o Redis no 3o Dev in SantosNeste sábado dia 5 de Maio participarei do maior encontro de desenvolvedores da baixada santista, falando um pouco sobre o Redis, e como o utilizamos pra tornar o desenvolvimento do <a href="http://musica.com.br/">Musica.com.br</a> mais fácil, estável e seguro.<br />
<br />
O <a href="http://www.mktvirtual.com.br/mailing/2012/dev-in-santos/">programa completo do Dev in Santos</a> com todas as apresentações você pode ver aqui: <a href="http://www.mktvirtual.com.br/mailing/2012/dev-in-santos/">http://www.mktvirtual.com.br/mailing/2012/dev-in-santos/</a>. Repare que teremos ótimas apresentações sobre uma grande variedade de assuntos como NodeJS, Python, IOS, Unity3D e é claro, Redis.<br />
<br />
Se você estiver pela baixada santista, ou até mesmo em São Paulo neste fim de semana, será uma bela oportunidade para bater um papo sobre desenvolvimento e outras nerdices. Não deixe de se <a href="http://www.meetup.com/devinsantos/events/58256142/">inscrever</a> enquanto ainda há vagas.<br />
<br />
Vejo vocês lá.<div class="blogger-post-footer"><script type="text/javascript"><!--
google_ad_client = "pub-8291320703218320";
/* 468x60, criado 06/03/09 */
google_ad_slot = "5590043178";
google_ad_width = 468;
google_ad_height = 60;
//-->
</script>
<script type="text/javascript"
src="http://pagead2.googlesyndication.com/pagead/show_ads.js">
</script></div>Tiago Albineli Mottahttp://www.blogger.com/profile/04371749360526831167noreply@blogger.com0tag:blogger.com,1999:blog-7709637092684013725.post-14010753593324983672012-04-23T17:35:00.000-03:002012-04-23T17:35:33.075-03:00Palestras no Flisol de SalvadorEste sábado, dia 28 de Abril estarei presente no <a href="http://softwarelivre.org/flisol-ssa">Flisol, Festival Latino-Americano de Instalação de Software livre</a>, conversando um pouco sobre como nginx e algumas técnicas de otimização para web. Serão duas palestras que envolvem o mesmo tema:<br />
<br />
Na primeira debaterei com o público sobre algumas ferramentas utilizadas no <a href="http://musica.com.br">Musica.com.br</a> para tornar o portal mais dinâmico e rápido, sem sobrecarregar o servidor. Bibliotecas como Head.js, backbone.js e localStorage serão abordadas.<br />
<br />
A segunda falaremos sobre como escalar seu site com Nginx. Mostrarei alguns problemas que tivemos e como solucionamos eles em diversos produtos da <a href="http://globo.com">Globo.com</a>.<br />
<br />
A <a href="http://softwarelivre.org/flisol-ssa/programacao">programação completa do Flisol Salvador</a> você confere aqui: <a href="http://softwarelivre.org/flisol-ssa/programacao">http://softwarelivre.org/flisol-ssa/programacao</a>.<br />
<br />
Portanto, se estiver por salvador neste feriadão, não deixe de participar deste evento e puxar um papo comigo.<div class="blogger-post-footer"><script type="text/javascript"><!--
google_ad_client = "pub-8291320703218320";
/* 468x60, criado 06/03/09 */
google_ad_slot = "5590043178";
google_ad_width = 468;
google_ad_height = 60;
//-->
</script>
<script type="text/javascript"
src="http://pagead2.googlesyndication.com/pagead/show_ads.js">
</script></div>Tiago Albineli Mottahttp://www.blogger.com/profile/04371749360526831167noreply@blogger.com0tag:blogger.com,1999:blog-7709637092684013725.post-66770175598866943012011-03-22T22:56:00.000-03:002011-03-22T22:56:45.266-03:00Corrigindo o enconding retornado pelo Net::HTTP do ruby 1.9A String retornada pelo Net::HTTP.get no ruby 1.9 sempre é definida com encoding "ASCII-8BIT". Acontece que muitas vezes esse não é o encoding correto da String, e portanto, erros esquisitos podem acontecer. Um exemplo pode ser visto abaixo quando utilizei essa String em um erb:<br />
<br />
<div class="codigo"><pre>invalid byte sequence in UTF-8</pre></div><br />
Ou o seguinte, que acontece ao tentar encodar essa String para UTF-8:<br />
<br />
<div class="codigo"><pre>> minha_string.encode("UTF-8")
Encoding::UndefinedConversionError: "\xC3" from ASCII-8BIT to UTF-8</pre></div><br />
A solução para este problema é antes de encodar, definir o encoding correto da String. Para isso podemos utilizar o header "content-type" retornado na requisição. Se por exemplo ele retornasse 'text/xml; charset=ISO-8859-1', podemos converter a String da seguinte forma:<br />
<br />
<div class="codigo"><pre>> minha_string.force_encoding("ISO-8859-1").encode("UTF-8")</pre></div><br />
Para não ter que fazer isso a cada requisição HTTP, criei uma lib que altera o metodo body do HttpResponse. O código pode ser pego aqui:<br />
<br />
<a href="http://gist.github.com/882465">http://gist.github.com/882465</a><br />
<br />
Eu até pensei em alterar o metodo force_encoding da String para aceitar o valor do content-type, mas achei que isso seria dar muita responsabilidade para a String. Como o valor de content-type está apenas no ambito do HTTP, faz sentido colocar na classe Net::HttpResponse.<div class="blogger-post-footer"><script type="text/javascript"><!--
google_ad_client = "pub-8291320703218320";
/* 468x60, criado 06/03/09 */
google_ad_slot = "5590043178";
google_ad_width = 468;
google_ad_height = 60;
//-->
</script>
<script type="text/javascript"
src="http://pagead2.googlesyndication.com/pagead/show_ads.js">
</script></div>Tiago Albineli Mottahttp://www.blogger.com/profile/04371749360526831167noreply@blogger.com0tag:blogger.com,1999:blog-7709637092684013725.post-83327284779907816462010-11-03T10:36:00.003-03:002010-11-03T10:43:36.754-03:00Gem brazilian-rails em versões de rails antigasSe você estiver usando uma versão antiga do brazilian-rails é bem provável que ao tentar instalar esta gem ela comece a instalar também versões superiores do rails. Isso porque esta gem possui um conjunto de dependências definidas somente com o operador ">=". <br /><br />Ou seja, se você tentar instalar a versão 2.1.13 por exemplo, que funciona com o rails 2.3.2, ele tentará baixar a dependência da gem brdinheiro ">= 2.1.13", acabando por baixar a versão 3.0.0, que depende do activerecord 3.0.0, do rails 3. Nem preciso dizer que isso acabará dando merda.<br /><br />A solução é baixar todos os componentes independentemente sem suas dependências da seguinte forma:<br /><br /><div class="codigo"><pre>sudo gem install brnumeros --version=2.1.13 --ignore-dependencies<br />sudo gem install brdinheiro --version=2.1.13 --ignore-dependencies<br />sudo gem install brcep --version=2.1.13 --ignore-dependencies<br />sudo gem install brdata --version=2.1.13 --ignore-dependencies<br />sudo gem install brhelper --version=2.1.13 --ignore-dependencies<br />sudo gem install brstring --version=2.1.13 --ignore-dependencies<br />sudo gem install brcpfcnpj --version=2.1.13 --ignore-dependencies<br />sudo gem install brI18n --version=2.1.13 --ignore-dependencies<br />sudo gem install brazilian-rails --version=2.1.13 --ignore-dependencies</pre></div><div class="blogger-post-footer"><script type="text/javascript"><!--
google_ad_client = "pub-8291320703218320";
/* 468x60, criado 06/03/09 */
google_ad_slot = "5590043178";
google_ad_width = 468;
google_ad_height = 60;
//-->
</script>
<script type="text/javascript"
src="http://pagead2.googlesyndication.com/pagead/show_ads.js">
</script></div>Tiago Albineli Mottahttp://www.blogger.com/profile/04371749360526831167noreply@blogger.com0tag:blogger.com,1999:blog-7709637092684013725.post-73227382701101276412010-09-02T11:53:00.004-03:002010-09-02T12:09:04.979-03:00Solucionando IO bloqueante do mysql no rubyFui a uma palestra muito interessante sobre performance na Oscon. A palestra <a href="http://www.oscon.com/oscon2010/public/schedule/detail/13709">No Callbacks, No Threads: Async & Cooperative Web Servers with Ruby 1.9</a>, tratava do problema de IO bloqueante do driver do mysql para ruby, e como solução era proposto o uso de recursos como <a href="http://rubyeventmachine.com/">Event Machine</a> e <a href="http://www.infoq.com/news/2007/08/ruby-1-9-fibers">Fibers</a> do ruby 1.9. Contudo, a solução não ficava nada elegante, tornando a manutenção do código muito complicada. Embora hoje haja um esforço para tornar esse trabalho transparente, consegui obter o mesmo resultado em performance basicamente aumentando o número de processos a atenderem as requisições.<br /><br />Mas antes de explicar a solução é preciso demonstrar o problema. O caso é que embora tenhamos threads no <a href="http://ruby-lang.org/pt/">ruby</a>, alguns drivers como o do mysql são bloqueantes, ou seja, quando estão em uma operação de IO eles bloqueiam o processo inteiro, inclusive todas suas threads. Veja por exemplo o seguinte código:<br /><br /><div class="codigo"><pre>class TestesController < ApplicationController<br /> def index<br /> Thread.new { Teste.connection.execute("insert into testes (id) select sleep(2)") }<br /> render :text => 'ok'<br /> end<br />end</pre></div><br />Teoricamente ao fazermos a requisição a este controller de teste, a requisição não deveria durar os dois segundos de espera pelo retorno do insert ao mysql. Mas não é isso que acontece. As requisições acabam sendo enfileiradas pois o processo inteiro fica bloqueado a cada execução de query no banco. Isso pode ser comprovado utilizando o <a href="http://httpd.apache.org/docs/2.0/programs/ab.html">Apache Benchmark</a>.<br /><br /><div class="codigo"><pre>> ab -c 10 -n 10 http://localhost:3000/testes<br />...<br />Concurrency Level: 10<br />Time taken for tests: 20.514 seconds<br />Complete requests: 10<br />...</pre></div><br />É bom deixar claro que o bloqueio ocorre somente ao usar o método execute do driver, que é responsável por fazer atualizações no banco. Fazendo somente consultas, o bloqueio não ocorre.<br /><br />Na palestra em questão, foi demonstrado que utilizando <a href="http://rubyeventmachine.com/">Event Machine</a> e <a href="http://www.infoq.com/news/2007/08/ruby-1-9-fibers">Fibers</a> é possivel desbloquear o processo utilizando callbacks. No final o tempo total foi reduzido para 2 segundos e alguns milésimos. Esse mesmo resultado eu obtive configurando um nginx com passenger configurado com 10 forks. Uma solução bem mais limpa. São apenas dois parametros, um do <a href="http://nginx.net/">nginx</a>, e outro do <a href="http://www.modrails.com/documentation/Users%20guide%20Nginx.html">passenger</a>:<br /><br /><div class="codigo"><pre>worker_processes 10;<br />#...<br />http {<br /> passenger_max_pool_size 10;<br />}</pre></div><br />E o resultado do teste:<br /><br /><div class="codigo"><pre>> ab -c 10 -n 10 http://localhost/testes<br /><br />Concurrency Level: 10<br />Time taken for tests: 2.424 seconds<br />Complete requests: 10</pre></div><br />É claro que ainda sim a solução proposta na palestra é mais performática, até porque nela o consumo de memória é bem menor. Resta saber se essa economia vale a pena quando se pesa na balança o custo de manter um código mais complicado e os problemas que a concorrência podem trazer ao seu projeto. E para demonstrar o quão escalável é dividir as requisições em processos, fiz ainda um ultimo teste, com 1000 requisições, sendo 200 simultâneas, configurando o <a href="http://nginx.net/">nginx</a> e o <a href="http://www.modrails.com/documentation/Users%20guide%20Nginx.html">passenger</a> para trabalhar com 200 forks:<br /><br /><div class="codigo"><pre>> ab -c 200 -n 1000 http://localhost/testes<br /><br />Concurrency Level: 200<br />Time taken for tests: 13.043 seconds<br />Complete requests: 1000</pre></div><br />Ou seja, o tempo total de teste manteve-se estável. Levando- em conta que dificilmente alguém fará um insert no banco com sleep, acredito que esssa seja uma boa solução para o problema de IO bloqueante. Ao invés de threads, utilizar processos.<div class="blogger-post-footer"><script type="text/javascript"><!--
google_ad_client = "pub-8291320703218320";
/* 468x60, criado 06/03/09 */
google_ad_slot = "5590043178";
google_ad_width = 468;
google_ad_height = 60;
//-->
</script>
<script type="text/javascript"
src="http://pagead2.googlesyndication.com/pagead/show_ads.js">
</script></div>Tiago Albineli Mottahttp://www.blogger.com/profile/04371749360526831167noreply@blogger.com3tag:blogger.com,1999:blog-7709637092684013725.post-38305135094214073872010-08-09T13:24:00.000-03:002010-08-09T13:25:19.228-03:00Aumento de produtividade por ponto de complexidadeTenho ouvido ultimamente muitos amigos da área comentando sobre pressão por aumento de produtividade baseada em pontos de complexidade. Isso me deixa bastante preocupado. Embora seja nobre o desejo de aumentar as entregas da área de desenvolvimento, quantificar isso usando os pontos de complexidade das histórias não quer dizer muita coisa. <br /><br />Contudo antes de simplesmente reclamar contra a pressão, é preciso analisar as possiveis causas de produtividade baixa que possam estimular esse tipo de pressão. Pensando bastante cheguei a três possibilidades, e com elas, possiveis soluções, que seriam mais eficazes do que estimular aumento desse tipo de numero.<br /><br /><span style="font-weight:bold;">1</span>- Não há confiança de que o time esteja trabalhando em seu máximo. Ou seja, o agente gerador de pressão acredita que os integrantes estão fazendo corpo mole ou gastando o dia com amenidades ao invés de se focar na entrega do projeto. A pressão por aumento de pontos de complexidade pode até resolver esse problema temporariamente, mas também pode ser mascarado por integrantes do time que se sentem coagidos a fazer horas extras para cobrir as entregas esperadas. Ou seja, o problema só se resolve mesmo com conversas francas e com uma presença mais ativa do interessado. <br /><br /><span style="font-weight:bold;">2</span>- O time percebendo folga na iteração aproveita para melhorar a qualidade da entrega ainda mais. Esse tipo de preciosismo costuma acontecer com bastante frequência. Muitas vezes o desenvolvedor por ter mais tempo para pensar aproveita para implementar testes e fluxos mais rebuscados evitando bugs que no futuro tomariam o triplo do tempo, e o designer aproveita para criar e rebuscar as interfaces e assim encantar ainda mais o cliente. Neste caso a pressão pelo aumento de entrega de pontos de complexidade apenas estimula a diminuição da qualidade. Ou seja, embora haja um aumento imediato na velocidade, em pouco tempo ela cairá por causa das correções de bugs e dos ajustes visuais.<br /><br /><span style="font-weight:bold;">3</span>- O time é inexperiente ou não conhece a tecnologia adotada. Neste caso pressionar pelo aumento de entrega de pontos de complexidade de nada adianta. De certa forma, com o tempo, naturalmente as entregas serão maiores, ou em muitos casos menores pois o time começará a estimar com menos pontos, demonstrando assim a ineficácia desse numeros para medir produtividade. Para resolver este problema existem diversas opções como, organizar dojos, estimular programação em par, indicar livros e treinamentos, estimular a experimentação com tempo para projetos pessoais.<br /><br />Todas três possibilidades acima eu vi acontecer de perto nos times em que trabalhei. E quase sempre o problema foi resolvido sem utilizar os pontos de complexidade como parâmetro de medição. E vocês lembram de alguma outra causa de produtividade baixa? Tem idéia de como solucionar? Qual sua opinião sobre o assunto?<div class="blogger-post-footer"><script type="text/javascript"><!--
google_ad_client = "pub-8291320703218320";
/* 468x60, criado 06/03/09 */
google_ad_slot = "5590043178";
google_ad_width = 468;
google_ad_height = 60;
//-->
</script>
<script type="text/javascript"
src="http://pagead2.googlesyndication.com/pagead/show_ads.js">
</script></div>Tiago Albineli Mottahttp://www.blogger.com/profile/04371749360526831167noreply@blogger.com4tag:blogger.com,1999:blog-7709637092684013725.post-74084068423236600062010-05-17T19:14:00.009-03:002010-05-17T19:38:49.114-03:00Site de Passione estréia nova plataforma de novelas da Rede GloboEntrou no ar hoje a tarde o novo site de <a href="http://passione.globo.com">Passione</a>, a nova novela das oito da <a href="http://redeglobo.com">Rede Globo</a>. Mas por trás deste site está muito mais que um site. É o ínicio de uma <a href="http://redeglobo.globo.com/novidades/novelas/noticia/2010/04/passione-estreia-nova-plataforma-de-internet-no-dia-17-de-maio.html">nova experiência</a> de consumo da dramaturgia da <a href="http://redeglobo.com">Rede Globo</a> na internet.<br /><br />Para quem não sabe, depois de quatro anos na equipe de <a href="http://video.globo.com">videos</a> da <a href="http://globo.com">globo.com</a>, me engajei logo no inicio deste projeto no time dedicado a ele. Apesar de ser a mesma empresa, é um mundo totalmente novo. Novas tecnologias, novas formas de gestão, novas pessoas. Tudo novo. Foi e está sendo muito bom.<br /><br />E agora, com a entrega no prazo, e o feedback de dezenas de pessoas no twitter <a href="http://twitter.com/#search?q=passione">elogiando nosso trabalho</a>, só tenho a agradecer ao meu time que me acolheu tão bem, e a todos as equipes que nos ajudaram, seja nos dando suporte, seja nos dando dicas, seja nos incentivando. Sem vocês o resultado não seria igual. Parabéns a todos nós!<div class="blogger-post-footer"><script type="text/javascript"><!--
google_ad_client = "pub-8291320703218320";
/* 468x60, criado 06/03/09 */
google_ad_slot = "5590043178";
google_ad_width = 468;
google_ad_height = 60;
//-->
</script>
<script type="text/javascript"
src="http://pagead2.googlesyndication.com/pagead/show_ads.js">
</script></div>Tiago Albineli Mottahttp://www.blogger.com/profile/04371749360526831167noreply@blogger.com3tag:blogger.com,1999:blog-7709637092684013725.post-50871122627624841792010-03-03T20:03:00.017-03:002010-03-03T23:41:02.352-03:00Objetos fake em diversas linguagens para divertir minha vóQuase todas as linguagens que trabalhei possuem ferramentas para criar objetos fakes e assim auxiliar na construção de testes. Mas e se essas ferramentas não existissem? Estaríamos perdidos? Claro que não! <a href="http://martinfowler.com/articles/mocksArentStubs.html#TheDifferenceBetweenMocksAndStubs">Mocks e stubs</a> podem ser criados de diversas formas diferentes nas diversas linguagens. <br /><br />Esse é um ótimo assunto pra se discutir em um jantar de família. Quer coisa mais divertida que explicar pra sua avó como construir objetos fake em diversas linguagens? Por exemplo um simples stub que conta quantas vezes um método que notifica visualização de um filme em um serviço externo é chamado. Algo similar ao que o código abaixo faz. Diversão garantida!<br /><br /><div class="codigo"><pre>servico.should_receive(:notificar_visualizacao!).with(filme).once</pre></div><br /><span style="font-weight:bold;">Em Ruby</span><br /><br />Em <a href="http://www.ruby-lang.org/">ruby</a> podemos simplesmente criar uma nova classe em tempo de execução ou adicionar um método a um objeto qualquer. É a maneira mais simples de garantir um sorrisão da sua vó tamanha facilidade. No caso do exemplo abaixo resolvi adicionar métodos a um objeto qualquer e ao final executo o teste utilizando o <a href="http://rspec.info/">rspec</a>.<br /><br /><div class="codigo"><pre>servico_fake = Object.new<br />servico_fake.instance_eval do<br /> def notificar_visualizacao!(filme)<br /> @filme_visualizado = filme<br /> @quantidade_visualizacoes = quantidade_visualizacoes + 1<br /> end<br /> def filme_visualizado<br /> @filme_visualizado<br /> end<br /> def quantidade_visualizacoes<br /> @quantidade or 0<br /> end<br />end<br /><br />filme = Filme.new<br />filme.servico = servico_fake<br />filme.visualizar!<br /><br />servico_fake.filme.should be_equal(filme)<br />servico_fake.quantidade.should == 1<br /></pre></div><br /><span style="font-weight:bold;">Em Python</span><br /><br />Em <a href="http://www.python.org/">Python</a> é possivel alterar métodos de instancias em tempo de execução, mas não é possivel adicionar métodos a um objeto da classe <span style="font-weight:bold;">object</span>. Neste momento minha vozinha fica decepcionada. Para fazer algo semelhante ao que fizemos em ruby teríamos então que instanciar um objeto da classe original e só depois modificar o método. Ficaria mais ou menos assim:<br /><br /><div class="codigo"><pre>filme_visualizado = None<br />quantidade_visualizacoes = 0<br /><br />def notificar_visualizacao_fake(filme):<br /> global filme_visualizado, filme_visualizado <br /> filme_visualizado = filme<br /> quantidade_visualizacoes += 1<br /><br />servico_fake = ServicoExterno()<br />servico_fake.notificar_visualizacao = notificar_visualizacao_fake</pre></div><br />Esta opção passa a ser ruim quando o construtor da classe original executa algumas tarefas que são custosas para o nosso teste. Por isso, acho que em python o melhor para o este caso é criar uma classe em tempo de execução, deixando minha vó um pouco mais alegre, conforme exemplo abaixo:<br /><br /><div class="codigo"><pre>class ServicoFake(object):<br /> def __init__(self):<br /> self.filme_visualizado = None<br /> self.quantidade_visualizacoes = 0<br /> def notificar_visualizacao(self,filme):<br /> self.filme_visualizado = filme<br /> self.quantidade_visualizacoes += 1<br /><br />servico_fake = ServicoFake()<br /><br />filme = Filme()<br />filme.servico = servico_fake<br />filme.visualizar()<br /><br />assert servico_fake.filme_visualizado is filme<br />assert servico_fake.quantidade_visualizacoes == 1<br /></pre></div><br /><span style="font-weight:bold;">Em Java</span><br /><br />Uma forma "simples" de fazer em <a href="http://java.sun.com/">Java</a> é criando uma nova classe para herdar a original e sobrescrever somente o método desejado. Mas aí cairíamos no mesmo problema que discutimos sobre um possível construtor com código muito custoso na superclasse. E minha vó, que apesar de repetir muitas vezes as mesmas histórias, não gosta de ouvir as nossas repetidas.<br /><br />Uma alternativa que temos é extrair uma interface da classe original para que em nosso teste a gente possa implementar essa interface da maneira que quisermos. Ficaria mais ou menos assim:<br /><br /><div class="codigo"><pre>//ServicoExterno.java<br />public interface ServicoExterno {<br /> void notificarVisualizacao(Filme filme);<br />}<br /><br />//ServicoExternoHTTP.java<br />public class ServicoExternoHTTP implements ServicoExterno {<br /> public ServicoExternoHTTP() {<br /> //Faz um monte de coisas<br /> }<br /> public void notificarVisualizacao(Filme filme) {<br /> //Faz mais coisas ainda<br /> }<br />}<br /></pre></div><br />Então em nosso teste a gente cria uma classe implementando a interface recém criada:<br /><br /><div class="codigo"><pre>public class ServicoExternoFake implements ServicoExterno {<br /> public int quantidade_visualizacoes = 0;<br /> public Filme filme_visualizado = null;<br /> <br /> public void notificarVisualizacao(Filme filme) {<br /> filme_visualizado = null;<br /> quantidade_visualizacoes++;<br /> } <br />}<br /></pre></div><br />E depois, mesmo que neste ponto minha vó já esteje dormindo, a gente utiliza a classe fake criada no teste:<br /><br /><div class="codigo"><pre>ServicoExternoFake servicoFake = new ServicoExternoFake();<br /> <br />Filme filme = new Filme();<br />filme.setServico(servicoFake);<br />filme.visualizar();<br /> <br />assertSame(filme,servicoFake.filme_visualizado);<br />assertEquals(1,servicoFake.quantidade_visualizacoes);<br /></pre></div><br />Parece que minha avó não gostou da história. Ela começou tão <a href="http://infoblogs.com.br/frame/goframe.action?contentId=29088">dinâmica</a> e foi ficando cadas vez mais devagar. Sem dúvida eu deveria ter contato ao contrário.<div class="blogger-post-footer"><script type="text/javascript"><!--
google_ad_client = "pub-8291320703218320";
/* 468x60, criado 06/03/09 */
google_ad_slot = "5590043178";
google_ad_width = 468;
google_ad_height = 60;
//-->
</script>
<script type="text/javascript"
src="http://pagead2.googlesyndication.com/pagead/show_ads.js">
</script></div>Tiago Albineli Mottahttp://www.blogger.com/profile/04371749360526831167noreply@blogger.com2tag:blogger.com,1999:blog-7709637092684013725.post-90947552881440023312010-02-09T17:02:00.003-03:002010-02-09T17:14:48.119-03:00Upload de arquivo com selenium server no firefoxPara fazer upload de arquivo com selenium é preciso alterar seu selenium-server.jar liberando permissão para que seja possível manipular campos do tipo file por javascript. Para fazer isso, extraia o <span style="font-weight:bold;">selenium-server.jar</span>:<br /><br /><div class="codigo"><pre>jar -xvf selenium-server.jar</pre></div><br />Crie um arquivo chamado <span style="font-weight:bold;">user.js</span> no diretório <span style="font-weight:bold;">customProfileDirCUSTFF</span> contendo um código semelhante com o exibido abaixo:<br /><br /><div class="codigo"><pre>user_pref("signed.applets.codebase_principal_support", true);<br /><br />user_pref("capability.principal.codebase.p0.granted", "UniversalFileRead");<br />user_pref("capability.principal.codebase.p0.id", "http://localhost");<br />user_pref("capability.principal.codebase.p0.subjectName", "");</pre></div><br />Repare que a liberação de acesso é feita por host. No caso do exemplo estou liberando apenas para o ambiente local. Se desejar liberar outros hosts baixa adicionar outros conforme o exemplo abaixo:<br /><br /><div class="codigo"><pre>user_pref("signed.applets.codebase_principal_support", true);<br /><br />user_pref("capability.principal.codebase.p0.granted", "UniversalFileRead");<br />user_pref("capability.principal.codebase.p0.id", "http://localhost");<br />user_pref("capability.principal.codebase.p0.subjectName", "");<br /><br />user_pref("capability.principal.codebase.p1.granted", "UniversalFileRead");<br />user_pref("capability.principal.codebase.p1.id", "http://globo.com");<br />user_pref("capability.principal.codebase.p1.subjectName", "");</pre></div><br />Feito isso, basta gerar novamente o jar e executá-lo.<br /><br /><div class="codigo"><pre>jar cvfm selenium-server.jar ./META-INF/MANIFEST.MF -C ./ .<br />java -jar selenium-server.jar</pre></div><br />Já será possivel preencher o path de qualquer arquivo no campo como se ele fosse apenas um campo de textos. Isso no Firefox é claro.<div class="blogger-post-footer"><script type="text/javascript"><!--
google_ad_client = "pub-8291320703218320";
/* 468x60, criado 06/03/09 */
google_ad_slot = "5590043178";
google_ad_width = 468;
google_ad_height = 60;
//-->
</script>
<script type="text/javascript"
src="http://pagead2.googlesyndication.com/pagead/show_ads.js">
</script></div>Tiago Albineli Mottahttp://www.blogger.com/profile/04371749360526831167noreply@blogger.com0tag:blogger.com,1999:blog-7709637092684013725.post-65923611777844979212010-01-15T14:03:00.001-03:002010-01-15T14:03:42.434-03:00Diferenças entre ruby e python: Que perigoEm ruby:<br /><br /><div class="codigo"><pre>def a(z=[])<br /> z.push 'a'<br />end<br />a # retorna ["a"]<br />a # retorna ["a"]<br />a # retorna ["a"]<br />a # retorna ["a"]<br /></pre></div><br />Em python:<br /><br /><div class="codigo"><pre>def a(z=[]):<br /> z.append('a')<br /> return z<br />a() # retorna ['a']<br />a() # retorna ['a','a']<br />a() # retorna ['a','a','a']<br />a() # retorna ['a','a','a','a']<br /></pre></div><div class="blogger-post-footer"><script type="text/javascript"><!--
google_ad_client = "pub-8291320703218320";
/* 468x60, criado 06/03/09 */
google_ad_slot = "5590043178";
google_ad_width = 468;
google_ad_height = 60;
//-->
</script>
<script type="text/javascript"
src="http://pagead2.googlesyndication.com/pagead/show_ads.js">
</script></div>Tiago Albineli Mottahttp://www.blogger.com/profile/04371749360526831167noreply@blogger.com10tag:blogger.com,1999:blog-7709637092684013725.post-15233766753357758062009-11-25T18:18:00.006-03:002009-11-25T19:12:17.738-03:00Utilizando asserts para testar layoutsUm dos problemas encontrados em nossos testes de aceitação é a impossibilidade de validar a aparência exata do resultado final de uma determinada ação do usuário. Conseguimos validar com o <a href="http://watir.com/">watir</a> se determinada div possui um texto, se determinado link está presente, mas nada impede que eles estejam escondidos, por trás de outro div, ou com letras da mesma cor do fundo. É sempre útil, mas não necessáriamente exato, sendo sempre um ponto de falha. Ainda mais em um sistema como o que estamos trabalhando em que o visual para o usuário tem uma grande importância.<br /><br />Eu e o <a href="http://twitter.com/lquixada">quixadá</a>, mestre do javascript e meu par de hoje, trabalhamos em uma correção de bug visual, e como costumamos trabalhar com <a href="http://www.teachmetocode.com/screencasts/4">desenvolvimento outside-in</a>, chegamos ao dilema de como criar um teste para garantir que a falha existia. A solução encontrada foi inserir asserts dentro do código javascript, tal qual a funcionalidade de <a href="http://wiki.answers.com/Q/What_does_the_assert_keyword_do_in_Java">asserts do java</a>. O código da função assert ficou parecido com o mostrado abaixo:<br /><br /><div class="codigo"><pre>function assert(mensagem,valorDesejado,valorRecebido) {<br /> if( valorDesejado != valorRecebido ) {<br /> var html = '<div class="warning">' + mensagem + '</div>';<br /> $('body').append(html);<br /> throw mensagem;<br /> }<br />}<br /></pre></div><br />Em nosso caso, tinhamos que ter a certeza de que após um clique do usuário a barra de rolagem do elemento mantinha-se da mesma forma que anteriormente. Então o código ficou parecido com o mostrado abaixo:<br /><br /><div class="codigo"><pre>$.get( url, function ( responseHtml ) {<br /> var scrollAnterior = $('#opcoes').scrollTop();<br /> substituiOpcoes( responseHtml );<br /> assert( 'Deveria manter scroll igual', scrollAnterior, $('#opcoes').scrollTop() );<br />});<br /></pre></div><br />No teste de aceitação verificamos então que o texto 'Deveria manter scroll igual' não deveria aparecer. O código do step do <a href="http://cukes.info/">cucumber</a> utilizando o <a href="http://watir.com/">watir</a> pode ser visto abaixo:<br /><br /><div class="codigo"><pre>Then /^a barra de rolagem deveria permanecer na mesma posição$/ do<br /> @browser.text.should_not include('Deveria manter scroll igual')<br />end</pre></div><br />Com o teste pronto e falhando, aí sim corrigimos a função javascript <b>substituiOpcoes(html)</b> de forma a manter o scroll anterior.<br /><br />Não é uma solução perfeita. Estamos pesquisando uma melhor, como pode ser vista no post <a href="http://www.anselmoalves.com/2009/10/28/t-plan-robot/">Testes de aceitação automáticos para Flash com T-Plan Robot</a> do Anselmo. Mas enquanto isso podemos evitar alguns pontos de falha visuais que costumávamos ter que testar manualmente. Dado que nosso sistema possui 100% de cobertura de testes unitários, somados a 152 cenários de teste de aceitação que abragem 1568 passos, acho que estamos indo por um bom caminho.<div class="blogger-post-footer"><script type="text/javascript"><!--
google_ad_client = "pub-8291320703218320";
/* 468x60, criado 06/03/09 */
google_ad_slot = "5590043178";
google_ad_width = 468;
google_ad_height = 60;
//-->
</script>
<script type="text/javascript"
src="http://pagead2.googlesyndication.com/pagead/show_ads.js">
</script></div>Tiago Albineli Mottahttp://www.blogger.com/profile/04371749360526831167noreply@blogger.com1tag:blogger.com,1999:blog-7709637092684013725.post-23138947093224858382009-11-09T23:42:00.000-03:002010-11-09T23:43:01.006-03:00Web Astrologia: Duvidas, criticas e sugestõesUtilize esse espaço para publicar duvidas, criticas e sugestões ao site <a href="http://webastrologia.com.br">http://webastrologia.com.br</a> Sua contribuição será muito bem vinda.<div class="blogger-post-footer"><script type="text/javascript"><!--
google_ad_client = "pub-8291320703218320";
/* 468x60, criado 06/03/09 */
google_ad_slot = "5590043178";
google_ad_width = 468;
google_ad_height = 60;
//-->
</script>
<script type="text/javascript"
src="http://pagead2.googlesyndication.com/pagead/show_ads.js">
</script></div>Tiago Albineli Mottahttp://www.blogger.com/profile/04371749360526831167noreply@blogger.com1tag:blogger.com,1999:blog-7709637092684013725.post-18969189463768562272009-10-30T00:24:00.006-03:002009-10-30T00:41:03.096-03:00API rest para OpenSocial do Orkut com rubyA documentação da <a href="http://code.google.com/apis/orkut/docs/rest/developers_guide_protocol.html">API rest do OpenSocial do Orkut</a> detalha muito bem as opções e formatos de retorno disponíveis porém é um tanto vaga sobre como fazer a autenticação necessária para usá-la. Basicamente lá é explicado os parâmetros a serem enviados e que o protocolo é o <a href="http://delicious.com/chapundifornio/oauth">OAuth</a>. Então detalho aqui como obter por exemplo os dados de um usuário apartir desta API.<br /><br />Em primeiro lugar é preciso obter a consumer key e consumer secret de sua aplicação. Isso é feito gerando um token aqui: <a href="https://www.google.com/gadgets/directory/verify">https://www.google.com/gadgets/directory/verify</a>. Você deve colocar esse token dentro da tag <span style="font-weight:bold;">content</span> do xml descritor de sua aplicação e depois fazer a validação provando que é dono da aplicação. Com isso o Google irá lhe informar seu consumer key e consumer secret. Guarde eles com carinho.<br /><br />Depois, com a <a href="http://oauth.rubyforge.org/">gem oauth</a> instalada você deverá executar um código semelhante ao exibido abaixo, com a premissa de que as variaveis <span style="font-weight:bold;">consumer_key</span> e <span style="font-weight:bold;">consumer_secret</span> estão preenchidas com os correspondentes à sua aplicação. E que a variável <span style="font-weight:bold;">id</span> é o id do usuário do <a href="http://www.orkut.com">Orkut</a> que você está querendo conhecer melhor.<br /><br /><div class="codigo"><pre> consumer = OAuth::Consumer.new( <br /> consumer_key, <br /> consumer_secret, <br /> :site => 'http://www.orkut.com',<br /> :scheme => :query_string,<br /> :http_method => :get <br /> )<br /> <br /> request = consumer.create_signed_request(:get, <br /> "/social/rest/people/#{id}/@self?xoauth_requestor_id=#{id}") <br /> res = Net::HTTP.start('www.orkut.com', 80) do<br /> |h| h.request(request) <br /> end<br /><br /> puts res.body<br /></pre></div><div class="blogger-post-footer"><script type="text/javascript"><!--
google_ad_client = "pub-8291320703218320";
/* 468x60, criado 06/03/09 */
google_ad_slot = "5590043178";
google_ad_width = 468;
google_ad_height = 60;
//-->
</script>
<script type="text/javascript"
src="http://pagead2.googlesyndication.com/pagead/show_ads.js">
</script></div>Tiago Albineli Mottahttp://www.blogger.com/profile/04371749360526831167noreply@blogger.com0