druplicon

Drupal 7: Crear nuevas acciones para Views bulk operations (VBO)

VBO se puede alimentar de acciones desde tres lugares distintos: De las acciones del sistema, de las acciones que uno crea con rules (vean http://nodeone.se/da/node/775) y las acciones creadas por módulos de terceros.

En este caso vamos a hacer lo tercero.

El caso ideal para crear una action propia es cuando necesitas flexibilidad total. En el ejemplo que voy a compartir necesitaba que la acción tuviera valores por defecto:
vbo custom admin settings form
Y un formulario para que el usuario pueda escribir algo distinto antes de realizar la acción:
vbo custom user settings form

Hagamoslo!

El caso de uso es el siguiente: tengo un view con VBO que tiene que poder hacer una solicitudes de información para cada uno de los nodos (productos), pero dando la oportunidad de que el usuario pregunte algo conciso.

Para esto, la implementación tiene 3 actores bien diferenciados:

  1. La acción
  2. El formulario de configuración que exponemos a la hora de configurar la acción en VBO
  3. El formulario que le mostramos al usuario para que pueda personalizar su solicitud

1: La acción:

Declarar una nueva action en Drupal es fácil, solo necesitas el hook_action_info() y una función que haga de action:

<?php
/**
 * Implements hook_action_info().
 */
function mimodulo_action_info() {
  $info['mimodulo_send_solinfo_action'] = array(
    'type' => 'node',
    'label' => t('Enviar solicitudes de información'),
    'configurable' => TRUE,
  );
  return $info;
}

function mimodulo_send_solinfo_action($node, $context = array()) {
  _mimodulo_send_solinfo($node, $context['user_data']);
}

/**
 * Envía una solicitud de informacion por email.
 */
function _ecentros_send_solinfo($node, $user_data) {
//  $user_data['tu_diras'];
}

No voy a entrar en detalles porque pueden leer lo necesario en hook_action_info(). Solo comentar que hemos declarado una nueva acción que solo se puede usar en contenidos de tipo nodo, y que es configurable. Que sea configurable quiere decir que le vamos a proporcionar un formulario al usuario para que haga algo. En mi caso el formulario tiene un textarea para que nos diga sobre que quiere informarse.

2: El formulario para el administrador:

<?php
/**
 * Implememts HOOK_ACTION_views_bulk_operations_form
 */
function mimodulo_send_solinfo_action_views_bulk_operations_form($settings, $entityType, $settings_dom_id) {

  //establecemos los valores por defecto
  $settings += array(
    'tu_diras' => "",
  );

  $form['tu_diras'] = array(
    '#title' => t('Tu dirás'),
    '#type' => 'textarea',
    '#description' => t('Si quieres puedes proveer un texto por defecto para el usuario'),
    '#default_value' => $settings['tu_diras'],
  );

  return $form;
}

VBO nos proporciona el “HOOK_ACTION“_views_bulk_operations_form las comillas son porque no es estrictamente un hook tradicional, pero vamos, que usa el mismo patrón, solo que en lugar de usar el nombre del modulo como “HOOK”, usamos el nombre de la función de la acción. En este ejemplo el nombre de la acción es “mimodulo_send_solinfo_action“.

El resultado de este hook lo podemos ver cuando añadimos nuestra acción a las que va a tener un view (la primer imagen de este post).

3: El formulario para el usuario:

<?php
/**
 * Implememts HOOK_ACTION_form
 */
function mimodulo_send_solinfo_action_form($context) {

  $context['settings'] += array(
    'tu_diras' => "",
  );

  $form['tu_diras'] = array(
    '#title' => t('Tu dirás'),
    '#type' => 'textarea',
    '#description' => t('Si quieres saber algo en particular por favor indicanoslo aquí'),
    '#default_value' => $context['settings']['tu_diras'],
  );

  return $form;
}

/**
 * Implememts HOOK_ACTION_validate
 */
function mimodulo_send_solinfo_action_validate($form, $form_state) {
  //No hace falta validar
}

/**
 * Implememts HOOK_ACTION_submit
 */
function mimodulo_send_solinfo_action_submit($form, $form_state) {
  //Devolvemos un array formateado como a nosotros mas nos guste.
  //Este array se va a enviar a la action dentro de $context['user_data']
  return array(
    'user_data' => array(
      'tu_diras' => $form_state['values']['tu_diras']
    ),
  );
}

Como ya comenté antes, al declarar una acción, si esta lleva configuración (‘configurable’ => TRUE), tenemos que proporcionar un formulario. Para implementarlo es la mar de simple, solo tienes que tratarlo como un FAPI normal y tenes que tener el cuenta la forma en la que nombras el formulario HOOK_ACTION_form, su validacion HOOK_ACTION_validate y el submit HOOK_ACTION_submit.

En la declaración del formulario podemos ver que tenemos un parámetro “$context“. Bueno este parámetro contiene entre otras cosas, la configuración que hemos proporcionado en el momento que añadimos la action al view. Y la estamos usando para establecerle al usuario un valor por defecto.

Y ya está, tenemos una action que funciona.

Ah y acá está todo junto

<?php
/**
 * Implements hook_action_info().
 */
function mimodulo_action_info() {
  $info['mimodulo_send_solinfo_action'] = array(
    'type' => 'node',
    'label' => t('Enviar solicitudes de información'),
    'configurable' => TRUE,
  );
  return $info;
}

function mimodulo_send_solinfo_action($node, $context = array()) {
  _mimodulo_send_solinfo($node, $context['user_data']);
}

/**
 * Envía una solicitud de informacion por email.
 */
function _ecentros_send_solinfo($node, $user_data) {
//  $user_data['tu_diras'];
}

/**
 * Implememts HOOK_ACTION_form
 */
function mimodulo_send_solinfo_action_form($context) {

  $context['settings'] += array(
    'tu_diras' => "",
  );

  $form['tu_diras'] = array(
    '#title' => t('Tu dirás'),
    '#type' => 'textarea',
    '#description' => t('Si quieres saber algo en particular por favor indicanoslo aquí'),
    '#default_value' => $context['settings']['tu_diras'],
  );

  return $form;
}

/**
 * Implememts HOOK_ACTION_validate
 */
function mimodulo_send_solinfo_action_validate($form, $form_state) {
  //No hace falta validar
}

/**
 * Implememts HOOK_ACTION_submit
 */
function mimodulo_send_solinfo_action_submit($form, $form_state) {
  //Devolvemos un array formateado como a nosotros mas nos guste.
  //Este array se va a enviar a la action dentro de $context['user_data']
  return array(
    'user_data' => array(
      'tu_diras' => $form_state['values']['tu_diras']
    ),
  );
}

/**
 * Implememts HOOK_ACTION_views_bulk_operations_form
 */
function mimodulo_send_solinfo_action_views_bulk_operations_form($settings, $entityType, $settings_dom_id) {

  //establecemos los valores por defecto
  $settings += array(
    'tu_diras' => "",
  );

  $form['tu_diras'] = array(
    '#title' => t('Tu dirás'),
    '#type' => 'textarea',
    '#description' => t('Si quieres puedes proveer un texto por defecto para el usuario'),
    '#default_value' => $settings['tu_diras'],
  );

  return $form;
}

Chau!

PHP

Closures: mod_fcgid: stderr: PHP Fatal error: Using $this when not in object context in

Las closures son algo genial, y grandes frameworks como symfony las aprovechan para hacer magia prácticamente, pero son relativamente nuevas en el mundo de PHP, y hay veces que dan problemas.

Por ejemplo, el titulo de este post se genera cuando tenes un closure dentro e una clase (está dentro de un método de la clase). cuando querés acceder a $this desde dentro del closure te va a lanzar el error. Un ejemplo:

<?php
protected function buscarProcesa(&$text) {
  preg_replace_callback("/algo/s", function ($matches) {
    $this->procesarMatches($matches);
  }, $text);
}

En este ejemplo estamos queriendo usar el método $this->procesarMatches() desde dentro del closure, y va a fallar por dos motivos el primero es que los closures no pueden acceder a las variables que se encuentren fuera de si mismas, así que $this al estar fuera, es inaccesible. Pero tiene solución, usando use ($this) se puede pasar a $this dentro del closure:

<?php
protected function buscarProcesa(&$text) {
  preg_replace_callback("/algo/s", function ($matches) use ($this) {
    $this->procesarMatches($matches);
  }, $text);
}

Ahora bien, usando use ($algunavariable, $otravariable) las podemos meter dentro del closure, y mientras que no sea $this, todo va a funcionar bien, pero el caso es que necesitamos $this, y en el momento que hagamos function ($matches) use ($this) vamos a recibir el error que les comento.

¿Por qué?, bueno es una limitación que ha quedado de cuando implementaron closures en PHP. Algún día lo solucionaran, pero mientras tanto la solución es esta:

<?php
protected function buscarProcesa(&$text) {
  $self = $this;
  preg_replace_callback("/algo/s", function ($matches) use ($self){
    $self->procesarMatches($matches);
  }, $text);
}

Si miran bien lo que hicimos es asignar $this a $self y enviamos $self al closure… y así funciona todo sin problemas.

Una nota final: Cualquier método/variable que llames dentro del closure tiene que ser necesariamente publica, sino falla.

Chau!

nodejs

Desplegar una aplicación Node.js como servicio (demonio)

Cuando quise desplegar mi app, imagine que iba a ser fácil, que lanzaba el comando “node miapp.js” y punto, pero aprendí a palos que así no va la cosa.

Para desplegar una aplicación web, necesitas que funcione como apache por lo menos: que puedas arrancarla, pararla, reiniciarla, ver su estado, acceder a su log, que se reinicie si se produce un error, etc etc etc.

Busque por Internet y leí un montón de artículos y foros que te dicen como montartelo: haciendo scripts en /etc/init.d y combinándolo con monit para reiniciarlo si se cae, Usando Upstart, lanzando la aplicación con “node miapp.js &” (proceso en background pero es la peor solución que leí en años), y alguna que otra mas.

La mas razonable fue la de Upstart, aunque ninguna de las soluciones fue bastante clara o completa como para copiar, pegar y arrancar. Por eso hice este script que toma en cuenta todo lo que hace falta para montar tu app como un servicio:

description "Demonio para mi aplicación en Node.js"
author "Capy - http://ecapy.com"

env LOG_FILE=/var/log/node/miapp.log
env APP_DIR=/var/node/miapp
env APP=app.js
env PID_NAME=miapp.pid
env USER=www-data
env GROUP=www-data
env POST_START_MESSAGE_TO_LOG="miapp ha sido iniciada."
env NODE_BIN=/usr/local/bin/node
env PID_PATH=/var/opt/node/run
env SERVER_ENV="production"

######################################################
# A partir de aquí no debería hacer falta tocar nada #
######################################################

#Nuestro proceso espera hasta que el sistema este cargado
start on runlevel [2345]
#Y se detiene cuando el sistema se apague
stop on runlevel [016]

#Levanta el proceso automáticamente si se muere:
respawn
#limitamos el "respawn" a 99 veces con un timeout de 5s para que no intente levantar el proceso infinitamente.
respawn limit 99 5

pre-start script
    #Necesitamos asegurarnos que el path del pid exista antes de arrancar el proceso.
    mkdir -p $PID_PATH
    mkdir -p /var/log/node
end script

script
	#Seteamos NODE_ENV para que nuestra app se ejecute en modo production, development, test, etc.
	export NODE_ENV=$SERVER_ENV

	#El siguiente comando es el que realmente arranca el proceso:
	#Utiliza el usuario y grupo www-data (--chuid)
	#Asigna un pid y lo crea si no existe (--make-pidfile y --pidfile)
	#Cambia el directorio de ejecución a donde esté nuestra app de node (--chdir)
	#Ejecuta la app (--exec)
	#Envía cualquier output que genere nuestra app al log (>> $LOG_FILE 2>&1)

    exec start-stop-daemon --start --chuid $USER:$GROUP --make-pidfile --pidfile $PID_PATH/$PID_NAME --chdir $APP_DIR --exec $NODE_BIN -- $APP >> $LOG_FILE 2>&1

end script

post-start script
	echo $POST_START_MESSAGE_TO_LOG >> $LOG_FILE
end script

Puede parecer un poco largo, pero si lo lees detenidamente vas a ver que es bastante concreto:

  1. Autor y descripción: son parte básica de un script Upstart
  2. Variables de configuración para que no tengas que meterte entre el código para ajustar el script para cada nueva app que crees.
  3. Cuerpo del script.

Acá esta el mismo script sin comentarios:

description "Demonio para mi aplicación en Node.js"
author "Capy - http://ecapy.com"

env LOG_FILE=/var/log/node/miapp.log
env APP_DIR=/var/node/miapp
env APP=app.js
env PID_NAME=miapp.pid
env USER=www-data
env GROUP=www-data
env POST_START_MESSAGE_TO_LOG="miapp ha sido iniciada."
env NODE_BIN=/usr/local/bin/node
env PID_PATH=/var/opt/node/run
env SERVER_ENV="production"

######################################################

start on runlevel [2345]
stop on runlevel [016]

respawn
respawn limit 99 5

pre-start script
    mkdir -p $PID_PATH
    mkdir -p /var/log/node
end script

script
    export NODE_ENV=$SERVER_ENV
    exec start-stop-daemon --start --chuid $USER:$GROUP --make-pidfile --pidfile $PID_PATH/$PID_NAME --chdir $APP_DIR --exec $NODE_BIN -- $APP >> $LOG_FILE 2>&1
end script

post-start script
	echo $POST_START_MESSAGE_TO_LOG >> $LOG_FILE
end script

Uso:

Por cada aplicación en nodejs que quieras montar tenes que crear un archivo en /etc/init/. El nombre del archivo tiene que terminar en .conf. por ejemplo miapp-service.conf. Una cosa importante es que el servicio que estas creando se llama como el archivo (sin el .conf), por lo que en nuestro ejemplo se va a llamar miapp-service.

Dentro de /etc/init/miapp-service.conf pegas el script y personalizas todo lo que haya hasta la linea divisoria.

Un par de aclaraciones:
env APP=app.js define el nombre del “index” de tu aplicación, y por lo general se usan o app.js o server.js. Pon el nombre de tu app sin el path hasta ella. Si tu aplicación lleva parámetros podes encerrar en comillas dobles algo asi como “env APP=app.js –extrasettings ../settrings.json”
env APP_DIR=/var/node/miapp el path hasta tu aplicación
env PID_NAME=miapp.pid pon el nombre que quieras, pero que sea único. por ejemplo, si tu app es una pagina web pon el nombre de tu pagina “mipaginaweb_com.pid” o algo así.
env USER=www-data y env GROUP=www-data no sería muy responsable usar root para ejecutar tu aplicación salvo que esta si que necesite estar en root, pero en el caso de que sea una pagina web, usa el usuario y grupo www-data así podes unificar criterios. Es solo una sugerencia, yo para mi app uso www-data aunque podes usar el grupo y usuario que te parezca mejor.
POST_START_MESSAGE_TO_LOG es solo un mensaje que se envía al log de la app cuando esta arranca.
NODE_BIN indica donde esta ubicado node, por lo general está en /usr/local/bin/node aunque si no estuviera allí podes cambiar este parámetro.
env SERVER_ENV=”production” Posiblemente tu aplicación web utilice entornos de “development“, “test” y “production“. Bien, especificalo acá.

El resto del script está comentado en código así que no añado nada.

Cuando hayas acabado ya vas a disponer de tu servicio y tratarlo como cualquier otro:

start miapp-service
stop miapp-service
restart miapp-service
status miapp-service

Espero que les sirva.

Chau!

nodejs

Preprocesar variables de templates en express framework (nodejs)

Bien, el caso de uso es el siguiente: tenemos varios templates en los que queremos mostrar mensajes al usuario si los hubiera.
Lo normal seria hacer algo así a la hora de renderizar un template:

Para la página principal:

app.get('index', function (req, res) {
    var messages = req.flash();
    var render_options = {
        titulo: "Página principal"
    };

    if (messages.error || messages.info || messages.success) {
        if (messages.error) messages.hasError = messages.error.length > 0;
        if (messages.info) messages.hasInfo = messages.info.length > 0;
        if (messages.success) messages.hasSuccess = messages.success.length > 0;
        render_options.messages = messages;
    }

    res.render('index', render_options);
});

Para la página del usuario:

app.get('user', function (req, res) {
    var messages = req.flash();
    var render_options = {
        titulo: "Perfil de " + req.session.user.name
    };

    if (messages.error || messages.info || messages.success) {
        if (messages.error) messages.hasError = messages.error.length > 0;
        if (messages.info) messages.hasInfo = messages.info.length > 0;
        if (messages.success) messages.hasSuccess = messages.success.length > 0;
        render_options.messages = messages;
    }

    res.render('index', render_options);
});

Bien, la idea se aprecia, constantemente tenemos que trabajarnos la variable “messages” y si hay mensajes la incluimos en las opciones de renderizado del template.

¿El inconveniente? el código se repite, lo que lo hace poco mantenible y ensucia. Solo por dar un ejemplo, podés ver que estamos preparando errores del tipo “error“, “info” y “success“, y si quisieras agregar un nuevo tipo de error llamado “warning“, te las verías p… negras. Bueno se entiende el inconveniente.

Solución: Encapsular en un preprocesador

Express nos da la posibilidad de preparar variables que van a parar al template haciendo uso de app.use() y res.locals.

NOTA importante: da igual el sistema de templates que uses: Jade, Hogan.js, Mustache, Handlebars, etc. Express es agnóstico en este sentido a la hora de preparar variables. Un puntazo.

Bueno, como decía, podemos agregar un app.use() en app.configure(function () {}); (suele estar en app.js, la base de tu aplicación).
Acá está el ejemplo. Hemos quitado la lógica de tratamiento de mensajes y la hemos colocado en este middleware (si, es uno de esos famosos):

app.use(function (req, res, next) {
    var messages = req.flash();

    if (messages.error || messages.info || messages.success) {
        if (messages.error) messages.hasError = messages.error.length > 0;
        if (messages.info) messages.hasInfo = messages.info.length > 0;
        if (messages.success) messages.hasSuccess = messages.success.length > 0;
        res.locals.messages = messages;
    }

    next();
});

Lo mas notable del código anterior es que hemos asignado el resultado a res.locals.messages. res.locals, va a “mergearse” con las variables que uses en res.render() antes de llamar al template.
NOTA: next(); es necesario para que el flow de ejecución siga adelante.

Hecho lo anterior, les presento las versiones simplificadas de los ejemplos que di al principio:

app.get('index', function (req, res) {
    var render_options = {
        titulo: "Página principal"
    };
    res.render('index', render_options);
});
app.get('user', function (req, res) {
    var render_options = {
        titulo: "Perfil de " + req.session.user.name
    };
    res.render('index', render_options);
});

Mucho mejor ¿no?

Chau!

CSS & maquetación

Crea tus iconfont a medida con IcoMoon

IcoMoon es genial, siempre que veo un iconfont o un pack de iconos armado con sprite tengo que sopesar si los KB que trae con sigo me va a compensar, al fin y al cabo suelo usar 5 o 10 iconos de los cientos que estos pack traen.

icomoon

Y acá es donde IcoMoon me enamoró. Podes seleccionar de entre un basto listado de iconos cuales querés y luego descargar un iconfont o iconpack que pese lo mínimo.

En este blog pueden ver en el footer que puse unos iconos muy lindos, pues son una iconfont que completa (css + .eot + .ttf + .svg + .woff) son 17.8kb (aunque estrictamente hablando en realidad por browser hablamos de una media de 5KB ya que no se cargan todas las variantes de la fuente).

Úsenlo que esta muy bueno!.
Chau!

Enlace a la APP

nodejs

req.flash() en express 3

A partir de la versión 3 de http://expressjs.com/ framework req.flash() fue quitado del core ya que estrictamente hablando, ésta funcionalidad no es “core”.

No pasa nada, seguimos teniendo los mensajes flash pero en un modulo aparte:

Para instalarlo:

npm install connect-flash

O bien lo agregas a tu proyecto expressjs dentro de las dependencias y lo actualizas con npm:

{
    "name": "application-name",
    "version": "0.0.1",
    "scripts": {
        "start": "node app"
    },
    "dependencies": {
        "express": "3.1.0",
        "connect-flash": "*"
    }
}
path/a/tu/app$: npm update

A partir de allí, su uso sigue siendo el mismo que en versiones anteriores:

Cargamos el modulo:

app.configure(function () {
    app.use(express.session());
    //flash(); SIEMPRE VA ENTRE express.session() Y app.router
    app.use(flash());
    app.use(app.router);
});

Y ya lo podemos usar. Por ejemplo para indicar a nuestro usuario que ha puesto mal sus datos de login:

app.post('/user/login', function(req, res){
  req.flash('error', 'Tu usuario o contraseña están mal!')
  res.redirect('/user/login');
});

app.get('/user/login', function(req, res){
 res.render('index', { messages: req.flash('info') });
});
performance

Mejorando el rendimiento en Drupal 7: Memcached y Entity cache

En este post vamos a enfocarnos sobre Memcached tratando de mostrar las ventajas a la hora de utilizarlo en Drupal 7.

Memcached para los que no lo conozcan, es un data cache basado en RAM.

Usar Memcached en Drupal tiene un beneficio altísimo tanto para usuarios logueados como para anónimos. ¿Por que?, fácil, Drupal para cachear utiliza la base de datos como almacén para dicho cache, y esto es costoso. Cada vez que guarda algo en cache o lo pide, estamos haciendo una query a la DB, y aunque toda tu DB este cacheada (mysql tiene su propio cache) Drupal siempre va a tener un importante overhead solo por tener que pedir a la DB este cache.

Técnicamente hablando el tema es así: Estés o no logueado, Drupal usa cache_get() y cache_set(). Luego, estas funciones delegan la tarea de cacheo a la clase que se encargue en ese momento de almacenar y devolver los datos cacheados. Una instalación estándar trae una clase llamada “DrupalDatabaseCache” que usa como backend storage la propia DB de Drupal. El módulo Memcache API simplemente proporciona su propia clase (MemCacheDrupal) que hace lo mismo pero mandando el cache a Memcache.

Memcache se presenta como una muy buena solución a este escollo ya que fue diseñado para ser usado como cache, ofreciendo un overhead mínimo y una velocidad de respuesta muy superior.

Bueno, vamos al lío.

Maquina de pruebas:

Para hacer las pruebas he usado la instalación de Drupal “Un mal día para este Drupal“.
Las pruebas las hice sobre usuarios anónimos, logueados y el propio admin que es el que mas suele sufrir en terminos de performance porque ejecuta código extra por los privilegios que conlleva.

Usuarios anónimos:

Pruebas con el cache de Drupal desactivado:

Sin Memcache

  • Executed 2312 queries in 199.42 ms.
  • Page execution time was 4791.87 ms.
  • PHP peak=59.25 MB.

Con Memcache

  • Executed 1226 queries in 71.77 ms.
  • Page execution time was 3892.39 ms.
  • PHP peak=57 MB.

Con Memcache y entity cache:

  • Executed 398 queries in 39.88 ms.
  • Page execution time was 3450.76 ms.
  • PHP peak=56 MB.
Ahora con el cache de Drupal y bloques activado:

Sin Memcache

  • Executed 2312 queries in 167.15 ms.
  • Page execution time was 4037.62 ms.
  • PHP peak=59.25 MB.

Con Memcache

  • Executed 1186 queries in 66 ms.
  • Page execution time was 3927.52 ms.
  • PHP peak=57.75 MB.

Con Memcache y entity cache:

  • Executed 377 queries in 30.31 ms.
  • Page execution time was 3534.07 ms.
  • PHP peak=55.25 MB.

Usuarios logueados

Pruebas con el cache de Drupal desactivado:

Sin Memcache

  • Executed 2324 queries in 162.27 ms.
  • Page execution time was 4033.2 ms.
  • PHP peak=58.75 MB.

Con Memcache

  • Executed 1237 queries in 73.47 ms.
  • Page execution time was 4028.68 ms.
  • PHP peak=56.25 MB.

Con Memcache y entity cache:

  • Executed 396 queries in 26.19 ms.
  • Page execution time was 3530.29 ms.
  • PHP peak=56 MB.
Ahora con el cache de Drupal y bloques activado:

Sin Memcache

  • Executed 2262 queries in 182.59 ms.
  • Page execution time was 4286.79 ms.
  • PHP peak=58 MB.

Con Memcache

  • Executed 1237 queries in 56.45 ms.
  • Page execution time was 3716.37 ms.
  • PHP peak=56.25 MB.

Con Memcache y entity cache:

  • Executed 379 queries in 27.97 ms.
  • Page execution time was 4072.24 ms.
  • PHP peak=55.25 MB.

Usuarios con rol “administrator”

Pruebas con el cache de Drupal desactivado:

Sin Memcache

  • Executed 3106 queries in 231.33 ms.
  • Page execution time was 5588.66 ms.
  • PHP peak=66.75 MB.

Con Memcache

  • Executed 1812 queries in 89.79 ms.
  • Page execution time was 4701.39 ms.
  • PHP peak=64.5 MB.

Con Memcache y entity cache:

  • Executed 1133 queries in 68.43 ms.
  • Page execution time was 4563.24 ms.
  • PHP peak=64.25 MB.
Ahora con el cache de Drupal y bloques activado:

Sin Memcache

  • Executed 3104 queries in 219.49 ms.
  • Page execution time was 5510.96 ms.
  • PHP peak=67 MB.

Con Memcache

  • Executed 1816 queries in 107.06 ms.
  • Page execution time was 4931.99 ms.
  • PHP peak=64.25 MB.

Con Memcache y entity cache:

  • Executed 1133 queries in 71.71 ms.
  • Page execution time was 4641.69 ms.
  • PHP peak=64.25 MB.

Resumiendo:

Gráfica comparativa de Memcache y Entity cache en Drupal

Como podemos ver, cuando usamos Memcache, las queries a la db bajan un nada despreciable 50%. Estas query que ya no están, son “cache” que pasan a ser gestionadas por Memcache, quien como ya comentamos, es mucho mas eficiente a la hora de servir contenidos cacheados (insisto que está diseñado para ello, ergo es muy difícil que mysql lo haga mejor).

Pero para mas inri, al activar Entity cache, automágicamente las query a la DB bajan un 60% mas sobre lo que ya había conseguido Memcache. Esto es gracias a que este modulo implementa una capa de cache a todas las entidades del core de Drupal mas todas las entidades creadas por terceros que implementen la api en entity cache para dejarse cachear.

Conclusiones:

  • Entre Memcache y entity cache hemos logrado una reducción media del 85% de accesos a la DB (awesomeeeee!).
  • El beneficio mas evidente en el uso de Memcache y entity cache juntos es el de una reducción drástica en las
    consultas que se hacen a la DB, lo que viene especialmente bien ya que cuando una pagina en Drupal comienza a
    recibir muchas visitas, la db es lo primero que sufre.
  • Luego de estas pruebas podemos concluir que si usar Memcache es una buena idea, combinarlo con entity cache es
    una excelente idea.
  • Memcache y entity cache funcionan tanto en usuarios logueados como anónimos.
  • Es evidente que mientras mas cosas cacheemos en Drupal, mejor va a funcionar esta solución. En especial me
    refiero a views, y panels. Usen el cache que estos módulos tienen, para que Memcache pueda gestionarlos. En una
    prueba rapida que hice conseguí rebajar aun mas la cantidad de queries a solo 95 contra las 377 que ya
    lográbamos usando Memcache y entity cache
    .

Happy performance!

Drupal scared

Un mal día para este Drupal

Hola, he preparado un Drupal 7 para hacer unas pruebas de rendimiento y me he ensañado todo lo que pude para que vaya lo mas lento posible.

¿Que le hice?

  • Habilite casi todos los módulos del core y todos los módulos mas usados mas las librerías externas, lo que nos da 202 módulos activos*1.
  • Aplique la traducción de todos los módulos al español (las traducciones son killer)
  • Hice un tipo de contenido con todos los field de los que dispongo (llamado “Full CT”).
  • 10000 nodos del tipo “Full CT”
  • 10000 términos
  • 10000 usuarios
  • 1000 links en main menu
  • Una vista con 100 usuarios por pagina y paginados
  • Una vista con 100 términos por pagina y paginados
  • Una vista con 100 nodos del ct con todos los fields.
  • Un panel con todas las vistas anteriores

Luego agarré todo lo anterior y lo coloqué en la pagina principal.

Para cuando terminé, Drupal estaba por los suelos. No servia, apenas respondía, los tiempos de carga eran penosos, vamos, lo que viene siendo mi entorno ideal para hacer las pruebas.

Lo pongo a vuestra disposición para quien quiera hacer sus propias pruebas.

Descargar

PD: Dentro de drupalbench.tar.gz está la DB, el usuario de Drupal es “admin” pass “admin”. La DB es drupalbench (toquen settings.php para configurar el usuario de mysql)

Referencias:

Módulos habilitados:

  • Aggregator
  • Block
  • Blog
  • Book
  • Color
  • Comment
  • Contact
  • Contextual links
  • Dashboard
  • Database logging
  • Field
  • Field SQL storage
  • List
  • Number
  • Options
  • Text
  • Field UI
  • File
  • Filter
  • Forum
  • Help
  • Image
  • Locale
  • Menu
  • Node
  • OpenID
  • Path
  • PHP filter
  • Poll
  • RDF
  • Search
  • Shortcut
  • Testing
  • Statistics
  • Syslog
  • System
  • Taxonomy
  • Toolbar
  • Tracker
  • Content translation
  • Trigger
  • Update manager
  • User
  • Standard
  • Address Field
  • Administration Development tools
  • Administration menu
  • Administration menu Toolbar style
  • Advanced help
  • Advanced help example
  • Backup and Migrate
  • Calendar
  • CKEditor
  • Coder
  • Coder Review
  • Colorbox
  • Commerce
  • Commerce UI
  • Cart
  • Checkout
  • Customer
  • Customer UI
  • Line Item
  • Line Item UI
  • Order
  • Order UI
  • Payment
  • Payment UI
  • Payment Method Example
  • Price
  • Product
  • Product UI
  • Product Pricing
  • Product Pricing UI
  • Product Reference
  • Tax
  • Tax UI
  • Commerce Features
  • Commerce Order Fieldgroup Panes
  • Context
  • Context layouts
  • Context UI
  • Bulk Export
  • Chaos tools
  • Chaos Tools (CTools) AJAX Example
  • Custom content panes
  • Chaos Tools (CTools) Plugin Example
  • Page manager
  • Stylizer
  • Views content panes
  • Date
  • Date All Day
  • Date API
  • Date Context
  • Date Popup
  • Date Repeat API
  • Date Repeat Field
  • Date Tools
  • Date Views
  • Devel
  • Devel generate
  • Email
  • Enabled modules
  • Entity API
  • Entity tokens
  • Entityforms
  • Entityforms Notifications
  • entityform_test
  • Entity Reference
  • Entity Reference Behavior Example
  • Features
  • Fieldgroup
  • NIF Field
  • Global Redirect
  • Google Analytics
  • Internationalization
  • Block languages
  • Contact translation
  • Field translation
  • Multilingual forum
  • Menu translation
  • Multilingual content
  • Path translation
  • Translation redirect
  • Multilingual select
  • String translation
  • Synchronize translations
  • Taxonomy translation
  • Translation sets
  • User mail translation
  • Variable translation
  • IMCE
  • IMCE Wysiwyg API bridge
  • Interval Field
  • jQuery Update
  • Localization update
  • Libraries
  • Lightbox2
  • Link
  • File entity
  • Media
  • Media Internet Sources
  • Memcache
  • Menu Block
  • Menu Block Export
  • Message
  • Message example
  • Message OG example
  • Migrate
  • Migrate UI
  • Module filter
  • Mollom
  • Nice Menus
  • Organic groups
  • Organic groups context
  • Organic groups register
  • Organic groups UI
  • Page Title
  • Panels
  • Panels In-Place Editor
  • Mini panels
  • Panel nodes
  • Pathauto
  • Profile2 translation
  • Profile2 pages
  • Profile2
  • Node Reference
  • References
  • User Reference
  • Rules
  • Rules UI
  • Rules translation
  • Rules Scheduler
  • Rules Forms Support
  • Special menu items
  • Token
  • Transliteration
  • Variable
  • Variable admin
  • Variable advanced
  • Variable example
  • Variable realm
  • Variable store
  • Variable views
  • Variable Email
  • Views
  • Views UI
  • Views Bulk Operations
  • Views Slideshow: Cycle
  • Views Slideshow
  • Views System
  • Webform
  • Wysiwyg
  • XML sitemap
  • XML sitemap custom
  • XML sitemap engines
  • XML sitemap internationalization
  • XML sitemap menu
  • XML sitemap node
  • XML sitemap taxonomy
  • XML sitemap user
  • Memcache Admin
1263055237_start-here-ubuntuoriginal

Añadir tu usuario de linux al grupo www-data

Cuando instalo un LAMP en algún Linux suelo añadir mi usuario (capy) al grupo www-data, cambio el directorio www al grupo www-data y le doy permisos de escritura a los usuarios del grupo para garantizarme que no vaya a tener problemas de permisos y cosas raras:

sudo usermod -a -G www-data capy
sudo chown www-data. /var/www/ -R
sudo chmod g+w /var/www/ -R

Y si no te da bola reinicia y se soluciona.

Chau!